Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spindle Mode setting ignored #422

Open
nnexai opened this issue Aug 15, 2019 · 39 comments
Open

Spindle Mode setting ignored #422

nnexai opened this issue Aug 15, 2019 · 39 comments

Comments

@nnexai
Copy link

nnexai commented Aug 15, 2019

I am currently trying to use a gShield on an Arduino Due running g2 for laser engraving. Cutting works quite well by using PWM and Spindle Speed Control for enabling/disabling the laser.

For engraving I just need a simple way to keep g2 from de-accelerating on every spindle speed change.

To my understanding this should be possible by setting SPINDLE_MODE "continuous" ( $spmo = 2 ) and SPINDLE_SPINUP_DELAY to 0

I then went ahead and generated gcode using a number of G1 movements. If no spindle speed changes are paired with the G1 commands everything runs smooth. By adding S0 / S1 to the G1 commands, the movement is stuttering (constantly accelerating and breaking for each change)

My understanding of the source code is code limited, but as far as I can tell the spindle mode is ignored for movement planning. In addition I am unable to pinpoint the lines responsible for those movements (most likely in one of the planner-files?).

I tried commenting out those mp_request_out_of_band_dwell(spindle.spinup_delay); lines, but since spindle.spinup_delay is already zero the command is already effectively a NOP.

Since I do not intent to switch tools and modes of my machine I would appreciate some help in fixing this problem even if it is just a crude workaround.

Currently running on the latest commit of the EDGE branch.

@justinclift
Copy link
Member

Interesting problem. Hopefully we can get this sorted.

@giseburt You're probably the best person to respond here?

Also @MitchBradley... any ideas? 😄

@MitchBradley
Copy link
Contributor

Sorry, I don't understand the code well enough to say anything helpful. I haven't looked at the interactions between spindle speed and planning. I never do on-the-fly spindle speed changes for my milling programs.
My laser engraver uses GRBL 1.1f on an Arduino Nano-based Mana SE setup. GRBL 1.1f does a great job of automatically coordinating the laser power with motion, eliminating over-burn at corners and preventing burn-through if the motion stops for whatever reason. I'm not thrilled with GRBL for milling, which is why I use g2core (Due + gShield + some external drivers) for my 4-axis milling setup. It would be nice to think that one program could do it all well, but the state of the world, as I see it, is that GRBL is the best for lasers, g2core the best for milling, and Marlin is the only game in town for 3D printing. (The Smoothieware people might disagree :-)

@justinclift
Copy link
Member

Heh Heh Heh.

With Marlin, are you using the 2.x (dev) stuff, with 6th order jerk control?

MarlinFirmware/Marlin#10337

Interested in hearing from people if it works well in practise. 😄

@MitchBradley
Copy link
Contributor

I have Marlin 2.x running on a couple of spare microcontrollers that are not hooked up to motors. I use it for testing CNCjs's Marlin interface. I have no idea how well it actually controls motors.

One impressive thing is the huge number of different chips they support. I got it running on a Teensy and an ESP32 with relative ease. That said, some features don't work on all platforms. Notably, immediate commands that bypass the queue are only supported on a few micros. I think that is because the bypass feature is implemented deep inside the serial port interrupt driver.

@nnexai
Copy link
Author

nnexai commented Aug 16, 2019

What I got so far is, that spindle speed changes (no matter if inline or standalone) are enqueued as BLOCK_TYPE_COMMAND.

BLOCK_TYPE_SPINDLE_SPEED exists but it looks unused to me.

So what I really need is a way to run the speed change in sync with the movement commands without it affecting planning at all.

patyork hardcoded those changes for the tinyg in his fork https://github.com/patyork/TinyG by setting entry and exit velocities according to the previous and next movement command but I am not quite sure how to change the planner accordingly.

I started work on supporting the spindle speed block type based on patyorks work but since my understanding on how the planner works is limited this might take a lot of trial and error.

@nnexai
Copy link
Author

nnexai commented Aug 16, 2019

Can't get my head around how commands are handled in the planner.. I just went in, added a new function to queue the spindle speed change, using BLOCK_TYPE_SPINDLE_SPEED, setting vmax values (cruise, exit, ...) to very high numbers and the length to 0. Then inside the linear planner I used the previous blocks exit velocity to overwrite cruise speed and exit velocity of the spindle speed block. Also setting hint to PERFECT_CRUISE (also tried the generic COMMAND one - can't remember the full name atm)

Build and flashed it - no change in the way G1 with spindle speed changes is handled 🤨.

@nnexai
Copy link
Author

nnexai commented Aug 16, 2019

At least skipping the planner and executing the changes immediately does work (in terms of smooth movement). Did this just for sanity checking because I find it kind of funny, if all changes I make presumably do nothing to the way these commands are handled...

@nnexai
Copy link
Author

nnexai commented Aug 16, 2019

hmm i don't think fixing commands is the way to go for me ... there are just too many dependencies between previous and next blocks in the planner for me to figure out... maybe i will just find a way to modify the movements following an S command to include the change upon entry ... this would reduce the the required modifications a lot

@giseburt
Copy link
Member

giseburt commented Aug 16, 2019 via email

@nnexai
Copy link
Author

nnexai commented Aug 17, 2019

Thanks for the in depth explanation giseburt. The things I tried yesterday involved exactly those lines. I differentiated between generic commands and spindle speed changes, but I was unable to get a smooth transition (maybe I didn't get all the places). For my tests I explicitly used different speeds without disabling the laser (S0) - since this introduces dwelling.

What I now have kind of working is the second method I described earlier (combining the speed change with the aline move). This works really well, but I need to work around some other expected problems I have introduced (for example atm I cannot set spindle speed without a move and only G1 is supported).

Also I am testing for speed changes in every aline segment.

Before: https://youtu.be/SnWvNd15bGk
After: https://youtu.be/7UEqXo4tG5s

@giseburt
Copy link
Member

The improvement is noticeable.

So the way I felt it should work, from a high level, is still move based. Much like with a PDF or on-screen graphics, you can change the drawing color/style all you want, but nothing happens until you tell it to draw a line.

So, a line with just a S1 would do nothing but set the “speed” (power) of the next move (assuming the “spindle” is turned on). Then, once a move comes it (or is on the same line, since the words in gcode are unordered on a line) the speed/power setting in effect will be attached to that block. The block is planned as any other move (no command used).

Later, during the prep of the sections (head, cruise, tail) the speed setting is applied using previously configured settings (velocity-to-pulserate curves, etc) to compute how many pulses-per-step and pulse duration of the laser. Then when the move is passed to the step generator, the laser is pulsed on the step clock as well, using instead of using a loosely-related PWM an actual pulses-per-step is used, making it so that no location on the material is pulsed longer than configured.

Likely the easiest way to do this is to add a “virtual stepper” (like the hobby servo) with one huge exception: instead of it being driven by the cartesian planning, it’s instead driven as linear “movements” computed in the prep stage. I’d suggest putting it at the top of _exec_aline_segment() to compute the “distance” that the laser “motor” will “move" (enough with the quotes) as a simply loading it with the current position + the segment length (float segment_length = mr->segment_velocity * mr->segment_time;). The inverse kinematics will use the motor’s configured steps-per-mm to make it pulse the right number of times, making that pulses-per-mm.

The final component would be to make the laser motor stretch pulses. This is not that hard. The motors step_end() method will be called at the beginning of every tick of the step timer (even when there’s no motion!) so you could put a downcount in the laser motor that’s user configurable pulse-duration. The step clock happens once every 1/FREQUENCY_DDA seconds.

For bonus points you could use the spindle start/stop to tell the laser “stepper" to start/stop (instead of relying on a secondary external signal to enable/disable the laser electronically).

@MitchBradley
Copy link
Contributor

I agree with what Rob said in the laser case where it is dangerous to turn on the laser when not moving, but in the milling case, I want/need to turn on the spindle independently of moving.

GRBL handles that dichotomy with a "laser mode" configuration setting. In laser mode, M3 (normally spindle on CW) and M4 (spindle on CCW) have different meanings. M3 means "Sn is not coupled to motion - just set the laser power immediately when the S word is processed", while M4 does the smart stuff - the laser is off when not moving, and the power is dialed down when slowing for a corner.

I would suggest using the GRBL approach if possible. Why create yet another dialect difference in the already-crazy GCode world?

@giseburt
Copy link
Member

I don’t have an issue with a GRBL-compatible laser mode. There are several dialects, and several things that none of them currently support that I feel we should.

What I don’t want (beyond initial coding and testing) is a whole-system switch to/from laser mode or any given dialect. We have been aiming at using the toolhead concept to allow tools to be configured by the toolhead type that they belong to. In this case, the tool is a “laser” toolhead, and that toolhead would be configured to use GRBL-compatible spindle controls. Another option would be the raster mode linked to above. In all cases we would want velocity-based pulses, either PPI (pulses-per-inch, an industry term) for raster work or cutting or power-level ramping for cutting.

Note on power level ramping: this means driving a secondary pin with a PWM waveform that controls the laser’s power level, not pulsing the laser on and off but instead communicating to the power supply of the laser much like a brushless ESC is controlled. Many lasers (like LED-based lasers) do not have this capability due to the nature of how they operate, but some lasers like CO2 lasers do. When you can control the power level, then you can cut without pulsing the laser by holding it on and ramping the power level relative to velocity. (Otherwise you’ll get burns at sharp corners.) In this mode the same gcode tells it desired power levels (presumably by overriding the spindle co tells for that tool), but instead of driving PPI it instead drives power level based on the velocity in each segment. Code wise this is not very complicated, and will likely be easier than PPI mode, but isn’t available on every laser type, so both must eventually be implemented.

@justinclift
Copy link
Member

Interesting. Is that to support multi-function machines? eg things that could (in one session) run a laser pass to (say) draw text on a part, then run a CNC pass to cut it out?

@giseburt
Copy link
Member

Yes. Each toolhead could be a different type. You could combine additive (3D printing) and subtractive (CNC) in the same machine. Or laser and pen. Or any number of combinations. The goal being that the gcode supports it (with a little help from JSON when needed).

@nnexai
Copy link
Author

nnexai commented Aug 27, 2019

Thanks for the in depth comments I will try and see what I can come up during the next days. (Currently mostly consumed by work on my 3d printer but I should have time during the weekend).

I like the idea of using the toolhead approach - maybe I will hack something together there.

What I found out for my setup is that using a resolution of 10 "pixels" per mm (possibly switching the laser every 0.1mm) I get slowdown for feedrates approaching 1000mm/min.

@jpbarad7
Copy link

I have been reading this thread with great interest, as I am in the process of using G2Core to replace the OEM control in a 40W Full Spectrum Laser.

nnexai, did you ever solve the issue with control of laser power during axis movement?

@mhlong10
Copy link
Contributor

mhlong10 commented Nov 23, 2019

Here is a video of g2core running on my CNC with a new 5.5 watt laser I just added.

I've modified the code so that tool 32 selects laser mode (would like to add an attribute to the actual tool so that this isn't bound to tool 32 but this just some quick code and it is easy to add this later). Additionally M4 selects dynamic power mode which adjusts the laser power based on velocity. It actually works much better than I expected. I used to get dots and burnt corners and now it is as clean as a whistle.

https://youtu.be/pZtGbeJ8giA

If people are interested I can post the code. I didn't fork the code just cloned edge. I haven't contributed code yet to this project so I'm not sure if I can just create a PR or do I need to fork.

Here is a screenshot from the video:

Screenshot from video

Here is something else I engraved with dynamic power:

IMG_5482

@MitchBradley
Copy link
Contributor

I like it! The result compares favorably to GRBL's dynamic laser power mode. I would like to see your patch.
One way to setup a PR is to fork this repo, then cherry-pick your mods onto the fork.

@mhlong10
Copy link
Contributor

Not sure if I used the correct process for getting the changes up, please suggest best practices. Changes are in #431

@mhlong10
Copy link
Contributor

mhlong10 commented Dec 1, 2019

I don't have a PR ready yet but I have proof-of-concept code which uses the 'W' axis to control laser power. Values between 0 and 1.000 mm translate to 0 - 100% power. It works well but when the gcode stream gets up to a couple hundred lines per second it starts to pause a bit but the dynamic laser control does a nice job of keeping it from burning. Overall the proof-of-concept is very workable. There is still much to do like have a way to "home" the laser. That is, there needs to be a way to zero the "stepper" value which is the laser power. For now I simply select tool 1 to take it out of laser mode and then issue a G0W0 which will sync the canonical and "laser motor".

Here is a video of it running. It's running at 1800mm/min, 50% maximum laser power, 0.25mm dot size, USB serial to a Raspberry pi.

https://youtu.be/UkHjyNFDmXA

In order to keep velocity issues out of the way I cranked the 'W' axis jerk rates up to 100,000 and the max feed rate to 100,000. This keeps the other axis as the dominant drivers of velocity... at least it appears that way.

Here is a close up of the finished work and a copy of the image used. Note I found a bug in the image to gcode translator which caused the obvious line on the arm along with other minor artifacts.

IMG_5502
scaled_image

@justinclift
Copy link
Member

That looks really nifty. 😄

@MitchBradley
Copy link
Contributor

Why W instead of S? GRBL uses S for laser power, and it seems better to adopt previous usage instead of further divergence in GCode dialects.

@mhlong10
Copy link
Contributor

mhlong10 commented Dec 1, 2019

This is just a PoC and so I wanted to test the ability of using a "laser motor" while minimizing code change. I see a couple of options, one is to translate 'S' into 'W' internally when in laser mode - which I consider a bit of a hack, the other is to add a new axis, which is more work but provides more upside.

I was also considering extending this implementation with a simple and possibly general protocol change (I don't like going outside the standard but just a thought). Basically it is to allow for a compressed set of values to be passed in for a parameter. The compressed set would always have to be last on a line and is not allowed to span lines. So going with the 'S' for laser power it would look like this:

G1X1.234Y5.678S<~9jqo^BlbD-BleB1DJ+*+F(f,q/0JhKF~>

The Z85 decoded values would be stretched across the distance traveled. So if for example the distance traveled is 10mm and there are 200 'S' values then each mm would contain 20 power levels. The code would interpolate (if needed) between these points as it traveled over the vector. This does limit the maximum amount to what can be contained in a single line but are still see huge benefits (assuming an average of 14 bytes per point "G1X123.456S100" vs 1.325 bytes per point "G1X123.456S<~...~>" == 265/200). 10x protocol savings.

This doesn't take into consideration defining the bit-depth of the compressed data or negotiating the maximum characters per line.

An advantage is that the height, width, ppmm, direction, transformation, overscan, ... need not be handled by the firmware. This can be handled more easily by the image to gcode translator.

Looking ta the code the pre-parser and gcode parsing could be changed relatively easily to accommodate this (at first glance. :) )... but again just getting familiar with all the code.

Just some thoughts. Also let me know if this discussion should be taken elsewhere.

@MitchBradley
Copy link
Contributor

The part I don't get is what is the use case for making the laser power a linear function of distance traveled. The synthetic case of an image with a linear gradient in grayscale intensity doesn't seem common enough to warrant such a feature. Perhaps the primary objective is to put the laser power word on the same line with the G1X.... motion?

I do see the value of the "compressed data" idea because that is basically a raster scan primitive. That said, shoehorning it into an existing GCode seems dubious. Perhaps it would be better to have a JSON command that does exactly what you want. That would give you the flexibility of adding as many control parameters as necessary, without messing with the GCode parser, risking the introduction of bugs there, making people say WTF? when they see a G1 command that doesn't match any existing standard or documentation for G1, etc.

If the JSON route seems too g2core-specific - even though it seems somewhat unlikely that this would be picked up elsewhere - you might consider the Marlin approach of structuring it as an M-code. Marlin has a gazillion wacky M-codes for doing 3d-printing specific stuff like tuning PID loops for extrusion temperature.

You could get some small protocol savings without departing from the standard by using G91 incremental mode, omitting the G1's (perfectly legal), and adding a new "incremental spindle" mode (G91.2). Instead of writing

M5
G1X123.30
M4
S100
G1X123.45
S103
G1X123.60
S109
G1X123.75S110
...

you would write

M5
G1X123.30
M4
S100
G91 (incremental axes)
G91.2 (incremental spindle)
X.15
S3
X.15
S6
X.15
...

Taking this a step forward, you could create an autostep mode invoked by, say, M222. In that mode, the above example would be written as:

M5
G1X123.30
M4
S100
G91.2 (incremental spindle)
M222 X.15 (begin autostepping with the indicated increment)
S3
S6
...
M222.1 (Cancel autostepping)
G90.3 (cancel incremental spindle)

@mhlong10
Copy link
Contributor

mhlong10 commented Dec 1, 2019

@MitchBradley - These are interesting ideas. I hadn't thought of an incremental mode via other M or G codes but this is basically where I was going. I completely understand not breaking the existing gcode parser. I didn't want to get too proprietary and was trying to keep change to a minimum.

There are a few things going on that I probably didn't explain well.

  1. As you mentioned, part of the objective was to include the laser power 'S' word as part of a G1X... motion. This was one of several reasons for using the 'W' axis for the PoC. It is already included on the line.
  2. Another reason for using the 'W' axis is that it interpolates the laser power across the movement. The movement in the x/y can be small enough that effectively there is no interpolation. In the PoC the image-to-gcode converter generates G1X... lines with increments of only 0.25mm. However if the power doesn't change "significantly" for many mms then it won't generate a new G1X.. line until needed. This method can be used where there are long runs with minimal inflection in power level, however it doesn't address high frequency power level changes.
  3. The compressed set of values idea wasn't a simple linear gradient across the entire movement but to allow for a list of arbitrary power levels at fixed intervals across the movement. This method works best for high frequency changes.

So using a combination of the second and third bullets above I think significant reduction can be achieved.

Getting back to your last example, how about:
...
M222 X.15S<~....~>
...
where the S values are instead contained in the M222 command and the autostepping mode is automatically "canceled" when the list is done. I'm not familiar enough with real world use of gcode but maybe the G91.2 isn't needed in this case. Or possibly the M222 isn't needed e.g. use G21.2X.15S<~...~>.

Thanks for listening and the advice.

@giseburt
Copy link
Member

giseburt commented Dec 2, 2019

Hi guys,

I’m hoping to drop a new release onto Edge (after moving edge into master) that has new spindle controls and tool work.

This will provide three things that are relevant to this conversation (but nothing different as far as gcode protocol goes):

1- Spindle controls broken into a different sub-module, so it can be redefined what M3/M4/M5 and the S word mean. The examples that I’ll provide are only a few different actual spindle types (i.e. PWM-based ESC controller, or 10V analog signal-based VFD controller, or completely external relay-based controller), but those spindle typed will have metadata-like options such as “requires a delay for this speed change,” etc.

2- The spindle commands will be stored in the gcode model and pushed to the planner. The spindle modules can optionally require that those are PTS (plan-to-stop) commands, of that they are merely quick spindle calls at movement load-time.

3- Each “tool” can use a different spindle type. So you could say that in this configuration tools 1-10 are a VFD-like spindle and tool 11 is a laser. Maybe tool 12 is a laser with different settings, like etching instead of cutting.

I will shortly after that work on a laser spindle, but will likely not be able to test it, so I’ll look to you guys (if you’re willing) to give it a go.

I plan on making it work as a combination motor (as in a subclass of Stepper), and a spindle (subclass of the unreleased Toolhead). That way it can take S and M3 (etc) commands and convert them to laser commands, but then execute them coordinated with motion as if it were an additional axis, similar to the W moves above except without the planner implications. It will require a minor modification of the kinematics (to take the move length and configure the laser to coordinate properly), but that’s easy since that’s also been made modular and extensible.

Anyway, I’ll drop a note here once the edge push is done, and we can discuss gcode protocol further as well once the primitives are in place.

@mhlong10
Copy link
Contributor

mhlong10 commented Dec 2, 2019

@giseburt Sounds like some very useful and substantial changes are on the way. I would be happy to help test or whatever else is needed.

Coordinating like the W moves above and getting the planner out of the way (like I've only partially worked around with ridiculously aggressive jerk and speeds for the W axis spindle control) would be great. It should simplify the code path for the spindle axis since nowhere near as much planning is needed.

I've been pleasantly surprised at just how well g2core performs. Even when using very short line lengths (.25mm ish) and speeds of 1800mm/min the system has worked well. It was only when pushed to 500,000 jerk and max velocity on all axis and driven at 2400mm/min that I started to have issues with something hanging in the planner (json commands still responded). Note that we are talking about short segments (about 6ms) and modified code so its not surprising something locked up.

@jpbarad7
Copy link

jpbarad7 commented Dec 4, 2019

mhlong10,

Thank you for excellent work on G2 laser power control; very impressive. I am in the process of rebuilding/heavily modifying a laser cutter and have some questions about your set up, programming changes, and the laser module you are using. Can you reach out to me privately via email to discuss?

Sorry to all for posting in this way - I was unable to find contact info elsewhere.

[email protected]

@mhlong10
Copy link
Contributor

mhlong10 commented Dec 5, 2019

@jpbarad7 If you are trying to modify an OEM laser then you are getting into more than I feel comfortable giving advice for. There are a lot of variables in the system and lasers are unforgiving and dangerous.

The laser module I'm using is. https://www.ebay.com/itm/450nm-7W-Blue-Laser-Module-With-Heatsink-For-Laser-Cutter-Engraver-CNC-DIY-Laser/392206265262

As far as hardware goes I have homemade CNC with VFD spindle I've been using for several months - more info is on my youtube channel. A couple weeks ago I added the above laser, basically mounted in onto the spindle housing and attached the PWM input to the spindle PWM output of the g2core shield. One thing to be careful with some of these modules is that they are "on" by default. I added a 680 ohm pulldown on the PWM on the laser and have an open-collector to +5v. This causes it to fail safely to "off".

The software changes and other information can be found in the the comments above and the PR. I didn't submit a PR for the raster changes ('W' axis as a laser) because it is not ready.... and given that @giseburt has a drop coming soon which significantly affects spindle/laser control, I'm going to wait until his new drop before I do much more work in this area.

One thing you will find if you decide to do raster laser printing before smooth laser motion is ready is that even with jerky movement, the dynamic laser control keeps the image from getting all screwed up.

@MitchBradley
Copy link
Contributor

@mhlong10 The "M222 X.15 S<...>" formulation doesn't fit into the core GCode syntax, where a GCode "word" is a letter followed by a number. I'm not keen on going outside core syntax. That would be hard for ancillary programs that parse GCode to cope with - programs like CNCjs, GCode previewers, and any number of others. Remember, it's not just g2core that looks at the GCode stream, it's the entire ecosystem.
That's why I suggested either completely sidestepping GCode by using a JSON command, or staying within the basic GCode framework at the expense of somewhat less bandwidth efficiency.
It's unfortunate that GCode is such a poorly designed language, but it is what it is.

@giseburt
Copy link
Member

giseburt commented Dec 5, 2019

I know it was mentioned above, but for those that skim, @aldenhart and myself have worked up a proposal for a laser mode here: https://github.com/synthetos/g2/wiki/Raster-Streaming-Protocol (It’s just that, a proposal, and open for discussion.)

One of the core concepts we try to maintain in g2core is compatibility with gcode parsers, even when we’re doing something that they clearly won’t interpret correctly, we can at least make it parse correctly. This is part of why we have active comments. We also pay close attention to how gcodes are used in other projects.

I’ve personally found that it’s helpful to think of gcode as a data format and not as a “language.” It’s closer to JSON and XML than to JavaScript or Python. You parse gcode and act upon the data found there, as opposed to interpreting it into byte code and eventually machine language. It’s a subtle distinction, but it helps me stay sane. 😁

@jpbarad7
Copy link

jpbarad7 commented Dec 5, 2019 via email

@jpbarad7
Copy link

jpbarad7 commented Dec 18, 2019 via email

@jpbarad7
Copy link

Standard settings
Dynamic settings

@mhlong10
Copy link
Contributor

@jpbarad7 I'm glad to see you have everything going and that the dynamic laser mode worked out for you. The results posted are great. I'm curious what feed rate you used for the stars. With the 5.5w laser I'm able to get similar results (on slightly darker card stock) with 30% power @ 2000mm/min and cut thru card stock at 100% power @ 1200mm/min. I'm also able to cut 3mm birch ply with 3 passes of 100% @ 500mm/min (proper focus is important). I've been very busy with my paying job and not had time to do much else. I hope to get back to this soon and add an acceptable raster laser mode as well.

@justinclift
Copy link
Member

... add an acceptable raster laser mode as well.

Whoo! That'd be awesome. 😁

@jpbarad7
Copy link

jpbarad7 commented Dec 19, 2019 via email

@mhlong10
Copy link
Contributor

@jpbarad7 You guessed it. :) I'm an engineer/programmer by trade and I love to dabble in it on the side as time permits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants