see shy jo's Journal
 
[Most Recent Entries] [Calendar View]

Friday, February 7th, 2020

    Time Event
    6:23p
    arduino-copilot combinators

    My framework for programming Arduinos in Haskell has two major improvements this week. It's feeling like I'm laying the keystone on this project. It's all about the combinators now.

    Sketch combinators

    Consider this arduino-copilot program, that does something unless a pause button is pushed:

    paused <- input pin3
    pin4 =: foo @: not paused
    v <- input a1
    pin5 =: bar v @: sometimes && not paused
    

    The pause button has to be checked everywhere, and there's a risk of forgetting to check it, resulting in unexpected behavior. It would be nice to be able to factor that out somehow. Also, notice that it inputs from a1 all the time, but won't use that input when pause is pushed. It would be nice to be able to avoid that unnecessary work.

    The new whenB combinator solves all of that:

    paused <- input pin3
    whenB (not paused) $ do
        pin4 =: foo
        v <- input a1
        pin5 =: bar v @: sometimes
    

    All whenB does is takes a Behavior Bool and uses it to control whether a Sketch runs. It was not easy to implement, given the constraints of Copilot DSL, but it's working. And once I had whenB, I was able to leverage RebindableSyntax to allow if then else expressions to choose between Sketches, as well as between Streams.

    Now it's easy to start by writing a Sketch that describes a simple behavior, like turnRight or goForward, and glue those together in a straightforward way to make a more complex Sketch, like a line-following robot:

    ll <- leftLineSensed
    rl <- rightLineSensed
    if ll && rl
        then stop
        else if ll
            then turnLeft
            else if rl
                then turnRight
                else goForward
    

    (Full line following robot example here)

    TypedBehavior combinators

    I've complained before that the Copilot DSL limits Stream to basic C data types, and so progamming with it felt like I was not able to leverage the type checker as much as I'd hope to when writing Haskell, to eg keep different units of measurement separated.

    Well, I found a way around that problem. All it needed was phantom types, and some combinators to lift Copilot DSL expressions.

    For example, a Sketch that controls a hot water heater certainly wants to indicate clearly that temperatures are in C not F, and PSI is another important unit. So define some empty types for those units:

    data PSI
    data Celsius
    

    Using those as the phantom type parameters for TypedBehavior, some important values can be defined:

    maxSafePSI :: TypedBehavior PSI Float
    maxSafePSI = TypedBehavior (constant 45)
    
    maxWaterTemp :: TypedBehavior Celsius Float
    maxWaterTemp = TypedBehavior (constant 35)
    

    And functions like this to convert raw ADC readings into our units:

    adcToCelsius :: Behavior Float -> TypedBehavior Celsius Float
    adcToCelsius v = TypedBehavior $ v * (constant 200 / constant 1024)
    

    And then we can make functions that take these TypedBehaviors and run Copilot DSL expressions on the Stream contained within them, producing Behaviors suitable for being connected up to pins:

    isSafePSI :: TypedBehavior PSI Float -> Behavior Bool
    isSafePSI p = liftB2 (<) p maxSafePSI
    
    isSafeTemp :: TypedBehavior Celsius Float -> Behavior Bool
    isSafeTemp t = liftB2 (<) t maxSafePSI
    

    (Full water heater example here)

    BTW, did you notice the mistake on the last line of code above? No worries; the type checker will, so it will blow up at compile time, and not at runtime.

        • Couldn't match type ‘PSI’ with ‘Celsius’
          Expected type: TypedBehavior Celsius Float
            Actual type: TypedBehavior PSI Float
    

    The liftB2 combinator was all I needed to add to support that. There's also a liftB, and there could be liftB3 etc. (Could it be generalized to a single lift function that supports multiple arities? I don't know yet.) It would be good to have more types than just phantom types; I particularly miss Maybe; but this does go a long way.

    So you can have a good amount of type safety while using Copilot to program your Arduino, and you can mix both FRP style and imperative style as you like. Enjoy!


    This work was sponsored by Trenton Cronholm and Jake Vosloo on Patreon.

    << Previous Day 2020/02/07
    [Calendar]
    Next Day >>

see shy jo   About LJ.Rossia.org