[atreus] Re: Atreus Firmware + Ragel logic

  • From: Fernando Febles Armas <fernandofebles@xxxxxxxxx>
  • To: atreus@xxxxxxxxxxxxx
  • Date: Wed, 31 Jan 2018 23:44:14 +0000

I wanted to change my four layers by using only the 'fn' key: not pressed,
hold, click or double-click. Where click is a transient layer that returns
to the normal layer after another keypress.

I implemented the logic inside the per_cycle() function: about 40 lines of
'if', 'else if' and several status variables and counters. It was 'almost'
working but sometimes the keyboard acted weird: sending the two characters
from different layers of the same key, or eating one key when two were
pressed quickly.

And more importantly, it felt wrong. Trying to implement something new
always ended with an unusable firmware and a lot of tests until I put it to
work again.

The classic solution for this kind of logic is to draw your states and
transitions, use a variable for the states, some as inputs for your
transitions, and write a (hopefully more clear) bunch of 'ifs' and 'elses'.

The problem is that even drawing a few states, you usually forget to draw
all the transitions for your input variables: the typical error is to do
something when a>0, another thing when a<0 and then forget the case a=0.
The real problems are usually more subtle and harder to spot.

Here is where a state machine compiler helps a lot. You define the state
machine with some code and the first thing you should do is to ask it to
'draw' a picture of your state machine. You look at it and is like... WTF!!
I didn't mean that. Then you realize that you forgot something or that you
enabled a strange loop. Without having to fry the Atmega and to press a lot
of keys trying to spot any problem. You can look at '
https://github.com/ferfebles/atreus-2leds1speaker-ragel' to see the
'picture' of my keyboard layer logic.

Usually, state machines have a 'string' or 'array' as input, a start state,
and at the end of the process, the current state is your output. Ragel is
very interesting because it allows you to:
- Process the input piece by piece
- Decide the next transition not only based on the input but on conditions
of other variables.
- Execute code when entering/staying/leaving states or transitions. Even
code that changes the input or the states.

This makes feasible to write a machine that is not meant to end, only to
process the next input piece, and modify the variables that you want. In
this case, the input is an 'int' with only two significant bits, one for
'fn' pressed and another for any alphanumeric key pressed. The output is
the layer that the keyboard should be in.

Ragel allowed me to add some logic easily:
- A disabled layer (layer -1), for those undefined moments where you still
don't know in which layer you should be (like when you just press the 'fn'
key and the state machine doesn't know if you are going to release it
quickly as a click, or you'll be holding it for a while)
- To store the first key pressed when in click layer, and only allow this
key until released because sometimes when writing quickly the next key
happened in the click layer.
- To disable the click layer by pressing again the 'fn' key after the
double click time has passed.

Some warnings if you want to use Ragel:
- It makes much more than this. It's very interesting (for other uses) that
it allows deciding your next transition based on a regexp of the input.
This can simplify your state machine a lot, and the documentation talks a
lot about it. But the most needed feature for my logic was to decide your
transition using conditions, and it was hidden in the last pages of the
documentation.
- You should use 'actions' for your code and comparisons inside Ragel (they
are like functions). Not only makes your code easier to read but allows
Ragel to identify the same code when you use it in different places and to
simplify the resulting state machine.

All the Ragel code is inside the 'layout_common.rl'. It gets compiled to
'layout_common.png' first, and then to 'layout_common.h' that is a cryptic
C code meant only for GCC.

The per_cycle() function only sets the input (kb_value) and calls the state
machine to parse it.

There are two changes in calculate_presses() inside 'atreus.c' to block
keypresses when layer_disable, and to allow only for the first_click_key
when you are in click_layer.

If you want to use this code, you should comment the 'set_leds()' function
(I modded the Atreus with two leds) and change 'layout.h' and 'layer2.h',
because I use a modified 'US International with no dead keys' layout under
Windows (this allows me to send Spanish accented keys in the click layer
and frees the single quote key from being a dead key)

That was a lengthy email. If you have any question or want to test the
firmware and there is any problem just ask.

On 31 January 2018 at 21:17, Christian Kellermann <ckeen@xxxxxxxxxxxxx>
wrote:

* Fernando Febles Armas <fernandofebles@xxxxxxxxx> [180131 20:38]:
I've been using your original firmware for more than one year. It was
easier to understand and to hack that TMK or QMK.

The hardest part for the state machine was to understand the Ragel
syntax.
Once you can 'paint' your states with the Ragel code (look into
layout_common.png) everything started to fit in.

And thanks again for putting your work and everyday support into the
Atreus
project.

Could you elaborate a bit more about what the SM does and why you
implemented it?

QMK is becomming more and more unwieldy, although I will probably
use it for my next split atreus build and everything less complex
seems to be better for me atm...

Thanks,

Christian

--
May you be peaceful, may you live in safety, may you be free from
suffering, and may you live with ease.


Other related posts: