monome / crow

Crow speaks and listens and remembers bits of text. A scriptable USB-CV-II machine
GNU General Public License v3.0
166 stars 34 forks source link

usb device lua REPL #8

Closed tehn closed 5 years ago

tehn commented 6 years ago

fully functioning usb device REPL:

investigate end character (newline?) to evaluate note screen/minicom will need to turning local key echo on?

trentgill commented 5 years ago

this is working on a very basic level. connect using minicom, default config. on linux was /dev/ttyACM0

takes around 20 seconds before the 'AT' commands settle (we can work around this in future). should print 'Dialing...' while it's waiting. then 'welcome to crow' and a '>' prompt once it's ready.

try print"hello" and see the response, then a new prompt. error messages are prefixed with '\'

@tehn i don't fuly grasp what you mean about returning a directly executable command to the host. this means we'd allow a crow script to directly execute lua code in a norns environment? this feels like an extension on the basics, so i think it should become a new issue if there's a specific use case you have in mind. i can see the value as a simple 'getter' though i guess.

tehn commented 5 years ago

holy moly!!!!

what i (think i) meant is for crow to be able to return actual text like:

crow_says("now.cv[3] = 1.3")

and norns would have a function crow_says (random name) that just grabs the string arg and executes it. edit: _c is probably a good optimized function name.

i need to think more about the interaction between the two systems based on the parameters and features we're anticipating. since we last spoke i've had to think wayyy more about callbacks etc while hacking norns.

so we need to figure out how to process incoming serial (coming out of crow). if it's just a REPL, we need to know, for example, when the data coming in is the return of a getter or poll.

ie: (i've added a > to indicate input/prompt)

> x = 3
<ok>
> x
3
> x()
lua: stdin:1: attempt to call a number value (global 'x')
stack traceback:
    stdin:1: in main chunk
> set_cv(1,1.0)
<ok>
> get_cv(1)
_c(ret_cv(1,1.0))

make sense?

so on the norns side (or whatever interface side) it can look for string token _c. if it doesn't exist, it just prints the output. if it does, it executes the code within the parens.

of course another way to do this would just be keyed values ie

_c(RET_CV, 1, 1.0)

and then make a parser, which would maybe make it easier to interface with non-lua clients. but i sortof love the idea of just writing callbacks ie on norns

function ret_cv(channel, value)
  crow.cv[channel] = value
  user_function()
end

user_function is basically an interrupt that can be used by a script.

in the crow code above, get_cv() is something like:

function get_cv(channel)
  print("_c(ret_cv(" .. channel .. "," .. crow.cv[channel] .. "))")

of course instead of _c as a token we should probably just use a weird character like ^^ followed by commands? (it's a tiny crow symbol!!)


sorry for the working-it-out-as-i-go

trentgill commented 5 years ago

for reference i just ran the first part, and we're partly there:

> x = 3

> x
"]:2: syntax error near <eof>

> print(x)
3

> x()
"]:1: attempt to call a number value (global 'x')

clearly i'm missing most of the error messages, but that shouldn't be too bad. not sure if the repl should be able to query the value of variable without having to explicitly print.

can you clarify that get_cv() is setting up a poll? to me the response to a query should just be:

> get_cv(1)
1.0

and that should be able to be called inline in your lua script (hopefully without blocking?). seems uncomfortable to query a value and have to setup a callback to receive it. could use coroutines or continuations behind the scenes to have fake-threading if needed.

// ok. i spent another 5 mins looking at it and i realize what you've written is quite elegant. i'd still love to do inline query, but i really like the idea of having a standardized way of defining & triggering a callback on norns (or other host).

ret_cv should be got_cv hah

i think we should write some more examples of what the potentials polls are before thinking of a communication protocol. first we need a very clear picture of what and how to introspect into the crow system.

tehn commented 5 years ago

to me the problem with:

> get_cv(1)
1.0

is that it's an asynchronous system. having the norns or whatever host explicitly wait for communication is a bad idea (hence your suggestion for coroutines, which i've still never investigated fully).

but also it requires setting up some sort of state machine. ie, norns/etc has to know that the next incoming value is something that should be treated a certain way. normally it's just print it (?) but in this case it'd have to use it for an assignment/etc. i don't want to have to write a complicated interpreter... i mean i'm even trying to avoid keeping track of KEY values for a serial protocol :)

what i think i'm trying to design is a way to both maintain REPL functionality and at the same time add an interpreter layer that another process (norns/m4l/etc) can interact with. so the ^^ token telling the receiver to PROCESS_THIS seems to make a lot of sense. everything is else just interactive noise (?)

we should write some more examples of what the potentials polls are before thinking of a communication protocol.

ohh yes indeed!

this is all very exciting

trentgill commented 5 years ago

i just realized that i was being quite dense about the use case you were describing with the getters & setters. i've been thinking pretty exclusively about writing on crow scripts, and thinking of the repl as a way to prototype that code. in this way i didn't correctly separate that your proposed method was for 2 systems communicating remotely. reading above i knew you were talking about this but the ramifications weren't following all the way through.

especially when thinking about crow as an extension to norns, it feels very natural to define callbacks in the crow system, that the user then implements in their norns script. we should structure the code / have some facility to have crow shoot all of it's callback definitions to norns upon query (or perhaps user-defined callbacks are sent at script start). this could be done with optional functions that a user can include for such purposes. of course the repl is a big help here too.

i will leave the question unanswered of exactly how the user interacts with the crow-repl (vs the matron-repl), but i think making the process as interactive as possible is great. i did some initial RAM tests and i'm confident we have nothing to worry about in terms of having things be self-documented on crow, and queryable via helper functions in the repl.

//

back to the topic, my distinction is that on crow itself (and thus, via the crow-repl as it is running on crow itself), the user should be able to query a value directly, which will fetch the value from the C slope generation class. this makes it possible to take a more functional approach without needing to explicitly store state. as such it's possible for an envelope generator to take a function which returns the current value of an input when it's called. see lua/crowlib.lua in the out section for an example of this.

this can be thought of as a 'stream' (or 'poll') set to the maximum rate, which saves the current input value to a variable called in.value which can then be queried at will. in reality in.value will use a metatable to fetch the value from C when it's called, but conceptually i think that's how it aligns to the crow-via-norns model.

// perhaps this is talking in circles. i'm just concerned to make the scripts as compatible as possible. having this conversation has made me realize that the processes are fundamentally different.

using crow as a hardware extension to a host, vs using a remote host to control crow through repl or by writing a script.

naturally your focus will likely be more on the former (hence the great callback declaration idea above), while mine is more on the standalone functionality.

//

i got tired of rambling and so spent some time fixing issues in the asl module which is now running on the chip and happily lfo-ing up and down. there's certainly issues in the slope generating code (it seems to wander out of range), but at least that's a nicely isolated problem.

but also! i just added the 'get_cv(channel)' function you sketched out and it works just fine. obviously the implementation is dead easy on crow, so we can feel free to rework that as time goes on.

also in implementing this i realized how it's far more difficult than i thought to have the call return the value directly. all crow can ever send back is text. it is a repl after all, and the only pieces on the remote host are the Read and Print. so any inline approach feels bizarre & would require a parser inside each getter which seems way overkill and predisposed to all kinds of errors.

the more i write lua the more it feels truly wacky and capable of all kinds of ridiculous things.

tehn commented 5 years ago

i've been thinking pretty exclusively about writing on crow scripts, and thinking of the repl as a way to prototype that code. in this way i didn't correctly separate that your proposed method was for 2 systems communicating remotely

ack, i should've been more explicit.

back to the topic, my distinction is that on crow itself (and thus, via the crow-repl as it is running on crow itself), the user should be able to query a value directly, which will fetch the value from the C slope generation class

this is absolutely correct and the right way to do things, as you propose.

i'm wondering how to design the two use cases together. for one thing, it'll be easy to have a remote_present() or whatever method for detecting if the USB client is attached. so for example when enabling an ADC poll at a specific interval we can also toggle whether the poll callback should also report the value over USB in addition to a user (in-crow) callback.

i'll revisit the command list and consider this, but would be curious to hear your opinions.


more broadly: crow use cases.

crow as remote

i suspect this will be the prominent use case as

  1. a native extension to norns
  2. max and m4l (ableton)

particularly because there will pre-made scripts and patches that people can just use without learning to script anything (unfortunately this is the market at large).

re: matron REPL integration. i don't plan on implementing some special mode as i don't think it's necessary. given the ^^command() interpreter scheme, norns can have a simple processor of incoming data from crow (print crow > (incoming string) or execute). on the outgoing side, norns will have a simple command to send serial via crow.command("set_dac(2,3.0)") for example, but then helper commands for syntax ie

function crow.dac(ch, val)
   crow.command("set_dac(ch,val"))
end

so in effect, the crow and norns REPLs will be fully integrated together just by the nature of the communication scheme.

you can always just _c = crow.command and then _c("x = 3") and then _c(x) should print crow > 3 (this is in theory).

so, crow is not just a cv interface, but a remote lua.

we need to figure out multi-line input, for sending a full function over. i'm not sure how even the normal command line REPL does this actually, in terms of understanding completion.

crow as local

primary access will be likely via a command line util (like screen, but with readline and some minor interfacing help, as mentioned on another git issue).

and then there's upload-to-flash, which is different than just putting a file into working memory.

this is where we'll need a state switch at the C level: serial_to_input vs. serial_to_flash

serial to input is normal REPL mode.

serial to flash is just taking incoming bytes and writing them to the lua program memory space, which will persist on power-down.

entering/exiting serial-to-flash mode should be interactive, ie. _flash_write() which calls c, flips the serial stream state.

when in flash mode the flash-writing function should check for a token ie --it-is-done at which point completion is understood.

the REPL command line util will wrap the file in this start/stop condition.

after a write-to-flash the LVM should reset fully (clear memory) and start back up, executing the flash mem.


of course, given this approach it'd be very simple to write a crow-script "librarian" that actually runs on norns itself-- if one simply wanted to attach the crow to norns to flash a new crow script.


in general, i think the common use for crow-as-remote will not be to have crow running custom code in addition to the standard library. ie, no real need to define an onboard callback if there is already a to-remote-callback which means the callback can be implemented on the host (norns/m4l) instead. for example... having extra metros running onboard the crow, instead of metros locally on norns/m4l.

we'll need some sort of command _is_flash_empty() which returns true/false to ascertain if the crow is in a known state, and if necessary _clear_flash() to get it to a known state.

for remote mode, the norns helper crow.init() may be something like check state, clear if needed.


oh man, no filter on this ramble, i'm sorry. need to categorize this all.

trentgill commented 5 years ago

repeating this for emphasis

so, crow is not just a cv interface, but a remote lua.

in general agree with all you've written. i've implemented stubs for the serial -> lua | flash already. what's needed is:

i was thinking the parser for switching serial stream destination would also check for a _goto_bootloader command, but if we have a _set_default_flash or similar command, this shouldn't be necessary, as the user can kill a non-behaving script.

for error checking i was thinking we stream serial to RAM buffer, restart the lua VM, and run loadstring() on the new program. then we run a simple lua program that tests the system is functioning (and not eg. in an infinite loop), which can timeout (resulting in error condition), or succeed, causing the program to be written to flash.

before running loadstring we could run a checksum which the command-line util would send at end of each upload. i believe stm32 has an on-board MD5 (or similar) calculator so this shouldn't be difficult to implement. i'm uncertain how reliable we can anticipate the serial communication to be. a simpler approach would be to have a failed upload simply retry once, and error out after second attempt.

//

in terms of the io between norns & crow, i think the approach you outlined is perfect. both systems send raw strings which can optionally be executed if the serial stream is prefixed with the ^^ symbol.

crow.dac(...) as a wrapper over crow.command("set_dac,..." is perfect.

side note: understood that the remote use case is likely far more typical, and agreed that predefined functionality is of utmost importance. of course i will continue to pursue these ideas more personally, but i want to prioritize making the typical use case as solid as possible. i'm hoping the majority of these simple norns & max/m4l functions can be made simultaneously accessible via the default script. this default script will ship in the binary itself, not in user-lua-space, so we can always jump to it.

i will spend some time thinking about those remote use-cases, perhaps more from the max/m4l side (leaving norns to you) so we can get closer to a command list that can be implemented.

also agreed doing callbacks on the host system makes most sense majority of the time. typically crow timers will be used for internal things like executing polls that have been requested by the host (eg get_adc)

tehn commented 5 years ago

oh oh i want to emphasize that i'm very interested in both use cases!

what is fun is to figure out how to make both work alongside one another seamlessly and elegantly, and it think the design feels good!