jschoch / ESPels

ESP32 electronic leadscrew for a lathe
Apache License 2.0
19 stars 4 forks source link

architectural possibilities #15

Closed digiexchris closed 1 year ago

digiexchris commented 1 year ago

Appologies for the long essay. This is an idea of how I'd like to reorganize the code base into C++ style concepts. I can submit PRs with very small proofs of concepts, perhaps converting a single small concept at a time, if that helps it make it clearer. I'll make more issues with ideas to change the machine functionality as I come across them, but this is specifically about the shape of the codebase in general.

I would like to spend some time if you're willing to let me converting some of this into c++ objects and separating the concerns and internal state into logical chunks. I think it would make future development a lot faster by calling getters and convenience functions on instances of an object rather than having to have to know about how the internal state of another area of concern works.

split headers and initializsers into a precompiled header, and then the initializer in the .inl. the PCH can then avoid the huge chain of includes at the top of every file.

For example, one main state machine class that handles the runmodes and other common states that multiple modules use. Other modules call functions to get and change state. Alternatively, an event based system as a later step 2.

Web calls State for basically an array (I'd make a DisplayOutput interface or something), rather than getting DRO position directly from memory. Then we can strip out Web and replace it with another implementation without touching State. Also, it allows us to make alternate interfaces that interact with State with the same api, but over a different method (such as, i2c to a backpack board or something).

Example workflow: Button press to change to SAE:

And, at runtime or compile time, we can turn on or off all of these features just by constructing the objects differently.

A couple of use cases this helps with:

-- Provides important improvements and easy to use mutexes for thread safety

Anyway, this is a request for comments with the offer of me doing most of the work if you would like. I don't expect you to drop everything for this :) My web front end skills are weak, but c++ is something I'm familiar with.

jschoch commented 1 year ago

Thanks for taking the time to explain this. I think some pseudo code would help us each understand our ideas. Stepping back a bit I have to admit i'm very much not a fan of OO and would prefer to do this in something functional like elixir/erlang, so we may be somewhat at odds here. Again, my C++ is very weak but I understand C++ can be more functional if you want.

Regarding having abstractions to swap in/out things that is fine but in my experience with buttons/displays etc it is a huge PITA and running this with the webUI makes dev go very fast and this will be my primary focus. The majority of state flows between the "controller" and the "interface" via arduinoJSON's documents. It is fine if those are structs or object (i'd prefer structs). I don't see enough concurrency for needing getters/setters but maybe you see a case where things need it. The document's need some cleaning up, I don't think i'm using all the values. translating the arduinoJson docs to another format seems like a low overhead thing and should be fairly easy to do.

The FSM currently only does a tiny bit of work, the majority of work is interrupt driven. The encoder ticks, interrupts, and the "gear" calculates the desired position and error and decides if a step is needed. If a step is needed the RTM gets it's instructions and takes care of turning the pulse on/off. there is a tiny bit of logic to ensure that steps get delayed after a dir_pin change.

a lathe that has a 127 tooth metric/SAE conversion gear) that is selectable in a gearboax or something easy to use, you can gain more accuracy and less error, just by setting up the Leadscrew object with the same values, just a different coordinate system and a different ratio and it can figure it out. Can define it as two leadscrews, and swap back and forth between them by their ID in a list. Maybe key the list by measurement system?

Right now the way i'd do this is by adding a bool to the json config object and then making the change during the setFactor() call.

https://github.com/jschoch/ESPels/blob/710b00d4c2ba6a7d826f24ede26724b2ffb06f9c/src/src/Controls.cpp#L88

This gets called every time the pitch is changed. BTW rapids are not used directly, it sets the pitch to the rapidPitch and then sets it back when the rapid is complete.

so the UI would issue something like

c.useMetricGear = true; // or c.useImperialGear
var d = {cmd: "jog",config: c}
// send over the websocket (this should be binary msgpack but I think it is just plain old JSON string, another thing to fix)
ws.send(JSON.stringify(d));

the commands can be seen here: https://github.com/jschoch/ESPels/blob/master/src/src/web.cpp#L457-L500

It is important to note that the statemachine just sets a flag for the interrupt to read and decide if it should move or not. The big case statment just gets everything ready. When the pos_feeding bool is true the machine typically waits for it's "start" (where ever the encoder happened to be when it was turned on) and when the start is reached it will start trying to figure out if it is where it should be or if it needs to step + or - but only when the encoder interrupt is firing. If the spindle is stopped nothing happens.

So all that said, it is just a tweak to setFactor, I don't think it needs a leadscrew object. perhaps an init, reconfig, and a set function are all that are needed. Once this is set process_motion() and do_pos_feeding() do all the work

https://github.com/jschoch/ESPels/blob/710b00d4c2ba6a7d826f24ede26724b2ffb06f9c/src/src/motion.cpp#L266 https://github.com/jschoch/ESPels/blob/710b00d4c2ba6a7d826f24ede26724b2ffb06f9c/src/src/motion.cpp#L171

Since this is abstracted in "gear" maybe we leave it there and just clean it up a bit? Gear has an init that is called before jogs/rapids. Take a look and see if you think gear is the place to move this logic.

-- Provides important improvements and easy to use mutexes for thread safety

i'm open to making mem access safer, perhaps it is my ignorance but I thought using volatile and IRAM_ATTR was enough. The main things that could have issues are the encoder values, the state, and the control bool flags. I'm not aware of multiple functions trying to write to the same vars, only reads of volatile typed values.

This brings up the naming thing. I started by just trying to get "jog" working but that working code got stuck with the name. It isn't really a "jog", a "jog" should move without the spindle moving and there should be several types of moves.

  1. Jog( speed, distance ) where the Z moves without the spindle moving. This is very rarely used.
  2. SyncMove( pitch,distance, handedness {CW || CCW}) where the Z moves in sync with spindle based on the current "gear" configuration and pitch setting. This assums the spindle will stay running in the same direction and the handedness of the spindle makes it a bit tricky. These moves are always relative to the current position.
  3. AbsoluteSyncMove( position ) this would move to a "DRO" position based on the current position.
  4. maybe needed AbsoluteJog(speed,position)

Personally I'd want to tackle this fix first because right now everything is named wrong ;)

digiexchris commented 1 year ago

I'm not opposed to functional at all. While I'm not very experienced working in it, I'm sure I could make my brain get it. The naming changes make sense, why don't we start there first, and document the existing architecture? Your explanation helped me understand what you were going for a lot.

My initial hiccup right now that kicked this idea off is seeing a large collection of variables similar to "toolposition" in a global namespace that doesn't really tell me what it's all used for, or how it could change and when. Or if it's even intended to be used internally by some group of code, or also allow access by other code.

but passing a struct with data into a function something like Control::CalculateMove(Move{ float distance, Direction direction}) (or the functional equivalent, to not be confused with other functions of a similar name in other contexts) might be easier to understand at a glance. It could use toolposition looked up internally or from global state and getting a known response back with an answer and not having to know what the function does internally with toolposition makes a little more sense to me. The function calls get optimized out at compile time anyway.

The volatile does give some protections for write collisions, but doesn't prevent bugs from two different things writing to it, or accidentally taking a reference of it instead of a copy, and writing back to it. c++'s unique_ptr helps a lot there, but I suspect there's a C version of that somehow. I know you're not using many pointers, but there are some subtle bugs around reference vs copy that can happen.

Hmm now that I think about it, what about just making more directories under src/src? Then we can extern only some things in the headers included by other contexts. everything else that is meant to be internal we can then guarantee it's only used by the file that owns it at link time. Maybe one step farther and use a naming convention for global state designed to be accessed by other contexts? Capital letter snake case maybe? Could enforce this with clangformat.

Anyway I'll bow to your preference and not go any further towards a c++ refactor. No problem!

jschoch commented 1 year ago

i'm open to using c++ magic, just don't want to hide the data behind objects and or getter/setter pattern. Agree on starting with naming.

A bit more on the current design. We can make changes and document them better so you can figure it out by reading the source.

The encoder has a "position", this is stored in the encoder object as pulse_counter but I access it with Encoder::getCount() so right away I'm proving myself a hypocrite ;) This position is the quadrature pulse count, not to be confused with the encoder raw pulses. The big thing that needs to be added is to be able to store, set, and retrieve offset "0" positions for the spindle. The main 2 use cases here are for threading without a compound and multi-start threads. The syncWaiting bool currently is used to tell the controller to wait till pulse_counter % encoder.start == 0 I'd like to be able to change encoder.start like G54..G59. start is currently fixed to the encoder cpr to avoid divide by 0 crashes.

Then there are toolRelPos, and toolRelPosMM. toolRelPos is the current tool position (z) calculated in stepper steps. toolRelPosMM is the current Z position calculated in mm. Confusingly these are the 2 main vars used. Again, this was hacked together and I obviously didn't think very hard about naming. We should rename these 2 as they are currently confusing, any ideas? Some thought is required here because I would like to be able to set multiple "zero" positions and be able to index them, kinda like G54-59 work offsets..

There are a bunch of other position things that should be cleaned up like targetToolRelPos and targetToolRelPosMM that likely aren't being used and need to be cleansed.

Then there are things that are poorly named like absolutePosition which is used to set the target position for an absolution position move but that name is hardly self descriptive.

Regarding code formatting, i'm sure you can tell that I just do what I feel like at the time, i can't make up my mind and i hop between erlang, elixir, ruby, c, and python enough to be quite confused at all times. I'm happy to run clangformat to pretty it up.