title | author | date |
---|---|---|
Programming Robots with Elm |
Anthony Deschamps |
September 26, 2018 |
(all the hard work I didn't have to do)
A Debian distribution for LEGO Mindstorms with API bindings for C, C++, Python, Go, JavaScript
https://github.com/ev3dev/ev3dev
- Can Elm be used to program a robot?
- Is it an effective language for robots?
- Is it a delightful language for robots?
Browser.sandbox :
{ init : model
, update : msg -> model -> model
, view : model -> Html msg
}
-> Program () model msg
Robot.program :
{ init : state
, update : Input -> state -> state
, output : state -> Output
}
-> Robot state
type alias Input =
{ lightSensor : Float, {- ... -} }
type alias Output =
{ leftMotor : Float, rightMotor : Float, {- ... -} }
Follow a line
update : Input -> State -> State
update input _ =
{ lightSensor = input.lightSensor
}
output : State -> Output
output { lightSensor } =
{ leftMotor = lightSensor
, rightMotor = 1.0 - lightSensor
}
port module Robot
port inputs : (Input -> msg) -> Sub msg
port outputs : Output -> Cmd msg
type alias Config state =
{ init : state
, update : Input -> state -> state
, output : state -> Output
}
program : Config state -> Robot state
program config =
Platform.worker
{ init = \_ -> ( { state = config.init }, Cmd.none )
, update = update config
, subscriptions = \_ -> inputs NewInput
}
update : Config -> Msg -> Model -> ( Model, Cmd Msg )
update config msg model =
case msg of
NewInput input ->
let
newState =
config.update input model.state
output =
config.output newState
in
( { model | state = newState }, outputs output )
function updateInput() {
app.ports.inputs.send({
lightSensor : lightSensor.reflectedLightIntensity,
// ...
});
}
function handleOutputs(outputs) {
leftMotor.start(Math.round(SPEED * outputs.leftMotor));
// ...
}
var app = Elm.Main.init();
app.ports.outputs.subscribe(handleOutputs);
setInterval(updateInput, 25);
- Follow a line around a track
- Grab things that you bump into
- Move them to the outside of the track
type alias Model =
{ perception : Perception
, behaviour : Behaviour
, control : Control
}
init : Model
init =
{ perception = Perception.init
, behaviour = Behaviour.init
, control = Control.init
}
update input { perception, behaviour, control } =
let
-- Perception
newPerception =
Perception.update input perception
-- Control
newControl =
Control.update newPerception control
-- Behaviour
( newBehaviour, maybeControl ) =
Behaviour.update newPerception newControl behaviour
in
{ perception = newPerception
, behaviour = newBehaviour
, control = maybeControl |> Maybe.withDefault newControl
}
- Have we encountered an obstacle?
- What direction are we going?
module Perception
type alias Perception =
{ time : Int
, claw : Claw.State
, bumper : Bumper
, wheels : WheelOdometers
, curvature : Curvature.State
, travelDirection : Maybe TravelDirection
, lightSensor : Float
}
update : Input -> Perception -> Perception
Just a boolean, but by a different name
update : Input -> Perception -> Perception
update input perception =
let
bumper =
if input.touchSensor then
BumperPressed
else
BumperUnpressed
in
{ bumper = bumper
-- ...
}
module Curvature
type Curve = Unknown | Straight | Left | Right
type State
= State
{ previous : Maybe { left : Int, right : Int }
, curve : Curve
, raw : Float
, average : Float
}
calculateCurve : Float -> Curve -> Curve
calculateCurve curvature current =
case current of
Straight ->
if curvature < -0.25 then
Left
else if curvature > 0.25 then
Right
else
current
-- ...
newCurve =
calculateCurve average state.curve
|> resetIfExtreme average
-- ...
resetIfExtreme : Float -> Curve -> Curve
resetIfExtreme curvature current =
if abs curvature > 0.7 then
Unknown
else
current
type TravelDirection
= Clockwise
| CounterClockwise
type alias Perception =
{ curvature : Curvature.State
, travelDirection : Maybe TravelDirection
-- ...
}
let
curvature = Curvature.update {- ... -}
travelDirection =
case Curvature.curve curvature of
Curvature.Unknown -> Nothing
Curvature.Left -> Just CounterClockwise
Curvature.Right -> Just Clockwise
Curvature.Straight -> perception.travelDirection
in
{ travelDirection = travelDirection, -- ...
}
- Often very functional
- Small state machines
- Sometimes computationally heavy
type alias Output =
{ leftMotor : Float
, rightMotor : Float
, clawMotor : Float
}
-- module Control
update : Perception -> Control -> Control
output : Control -> Perception -> Output
type Control
= Idle
| Grab Timer
| Release Timer
| FollowLine
| MoveTo { left : Int, right : Int }
| MoveBy { leftDelta : Int, rightDelta : Int }
idle : Control
idle = Idle
update : Perception -> Control -> Control
update perception control =
case control of
Idle ->
control
output : Control -> Perception -> Input -> Output
output control perception input =
case control of
Idle ->
{ leftMotor = 0.0
, rightMotor = 0.0
, clawMotor = 0.0
}
output : Control -> Perception -> Input -> Output
output control perception input =
case control of
FollowLine ->
{ leftMotor = perception.lightSensor
, rightMotor = 1.0 - perception.lightSensor
, clawMotor = 0.0
, lights = Nothing
}
update : Perception -> Control -> Control
update perception control =
case control of
Grab Starting ->
Grab (Since perception.time)
Grab (Since startTime) ->
if perception.time - startTime > grabDuration then
Idle
else
control
update : Perception -> Control -> Control
update perception control =
case control of
MoveBy { leftDelta, rightDelta } ->
MoveTo
{ left = perception.wheels.left + leftDelta
, right = perception.wheels.right + rightDelta
}
MoveTo { left, right } ->
if within 5 left perception.wheels.left {- -} then
Idle
else
control
- Often very functional
- Small state machines
- Layered
module Behaviour
type Behaviour
= Initializing { openedClaw : Bool, closedClaw : Bool }
| FindingObject
| CarryingObject
| RemovingObject (List Control)
module Behaviour
update :
Perception
-> Control
-> Behaviour
-> ( Behaviour, Maybe Control )
update perception currentControl behaviour =
-- ...
case behaviour of
FindingObject ->
case ( claw, bumper) of
( Claw.Open, BumperPressed ) ->
( behaviour, Just Control.grab )
( Claw.Closed, _ ) ->
( CarryingObject, Nothing )
_ ->
( behaviour, Just Control.followLine )
removeObject : TravelDirection -> Behaviour
type Behaviour
= RemovingObject (List Control) | -- ...
case behaviour of
CarryingObject ->
case perception.travelDirection of
Nothing ->
( behaviour, Just Control.followLine )
Just travelDirection ->
removeObject travelDirection
When we find an obstacle, we need to...
- Grab it
- Turn
- Move forwards
- Let it go
- Move back
- Turn back
update perception currentControl behaviour =
case behaviour of
RemovingObject steps ->
case ( Control.isIdle currentControl, steps ) of
( True, next :: remaining ) ->
( RemovingObject remaining, Just next )
( True, [] ) ->
findObject
( False, _ ) ->
( behaviour, Nothing )
- Stateful
- Symbolic
- Explainable
Yes!
- Is Elm an effective platform for robots?
- Is Elm an effective language for robots?
I think so!
Decide for yourself!
https://github.com/adeschamps/programming-robots-with-elm/
Slack: anthony.deschamps
Matt Griffith
Mike Onslow + Elm Detroit