// thinkbeforecoding

Applicative Computation Expressions - 3

2020-12-03T09:21:51 / jeremie chassaing

this post is part of the F# Advent Calendar 2020

In this third installment of our series about applicative computation experessions, we'll jump to practice with Lego Mindstorm Ev3. This should demonstrate a non trivial, real life use case for applicatives.

Even if you don't have a mindstorm set at home, follow this post. We'll mix binary protocols and category theory - lightly explained - to provide a nice and safe API.

Mindstorm control over bluetooth

A few years back, I wrote a quick fsx script to control lego mindstorm in F#. I worked again on it recently to port it to net5.0.

Once paired and connected, the brick - the central component of Mindstom used to drive motors and read sensors - appears as a serial port, COM9 on my machine. If you're using a linux system, it should appear as /dev/ttyS9 (or another number).

System.IO.SerialPort is available directly in Net framework, and as a nuget in netcore and net5.0. Add the reference to your project, or use the #r nuget reference in a F# 5.0 script:

1: 
#r "nuget: System.IO.Ports"

We will read/write bytes using the SerialPort and ReadOnlyMemory buffers. We'll soon go in more details.

Request/Response

The most interesting part is the Request/Response mechanisme used for sensors. We write the request on the serial port by using port.Write, but get the response through an notification of the port.DataReceived event.

The request response is asynchronous, and it would be far easier to present it in the API as an Async construct.

When sending a request, we must include a mandatory sequence number in int. This sequence number will the be transmited in the corresponding response for correlation.

1: 
type Sequence = uint16

We will use this sequence number and a MailboxProcessor to implement Async in a responseDispatcher. It accepts two kinds of messages.

Request contains a Sequence number and a callback to call when the response bytes are available.

Forward contains a Sequence number and corresponding bytes.

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
open System
open System.Threading
open System.Text
open System.Buffers
open System.Buffers.Binary

type Dispatch =
    | Request of sequence: Sequence * (ReadOnlyMemory<byte> -> unit)
    | Forward of sequence: Sequence * ReadOnlyMemory<byte>

The mailbox processor is using an async rec function to process messages. On a Request message, it adds the callback to a Map under the sequence number key. On a Forward message, it finds the callback corresponding to the sequence number and call it with the data:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
let responseDispatcher() =
    MailboxProcessor.Start
    <| fun mailbox ->
        let rec loop requests =
            async {
                let! message = mailbox.Receive()
                let newMap =
                    match message with
                    | Request(sequence, reply) ->
                        Map.add sequence reply requests
                    | Forward(sequence, response) ->
                        match Map.tryFind sequence requests with
                        | Some reply -> 
                            reply response
                            Map.remove sequence requests
                        | None -> requests
                return! loop newMap }
        loop Map.empty

The callback in the Request message is create using PostAndAsyncReply. The result is an Async that is competed when the callback is called.

With this in place we can implement the Brick:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
35: 
36: 
37: 
38: 
39: 
40: 
41: 
42: 
43: 
44: 
45: 
46: 
47: 
48: 
49: 
50: 
51: 
52: 
type Brick( name ) =
    // create the port
    let port = new IO.Ports.SerialPort(name,115200)

    // the dispatcher for request/response async
    let dispatcher = responseDispatcher()

    // the mutable sequence number to provide unique numbers 
    let mutable sequence = 0


    member _.Connect() =
        // open the port
        port.Open()
        let reader = new IO.BinaryReader(port.BaseStream)

        // register to recieve data notifications
        port.DataReceived |> Event.add (fun e ->
            if e.EventType = IO.Ports.SerialData.Chars then
                //the response start with the size
                let size = reader.ReadInt16()
                let response = reader.ReadBytes(int size)
                // and contains the sequence number as first 2 bytes
                let sequence = BitConverter.ToUInt16(response, 0)
                // we can then send it to the dispatcher
                dispatcher.Post(Forward(sequence, ReadOnlyMemory response))
            ) 

    // gets new sequence number
    member _.GetNextSequence() =
        Interlocked.Increment(&sequence) |> uint16

    // write to the SerialPort
    member _.AsyncWrite (data: ReadOnlyMemory<byte>) = 
        let task = port.BaseStream.WriteAsync(data)
        // check synchronously if the ValueTask is completed
        if task.IsCompleted then
            async.Return ()
        else
            // else fallback on Task
            Async.AwaitTask (task.AsTask())

    // send a request using specified sequence number
    // and await the corresponding response
    member this.AsyncRequest(sequence, data: ReadOnlyMemory<byte>) =
        async {
            do! this.AsyncWrite(data)
            return!  dispatcher.PostAndAsyncReply(fun reply -> Request(sequence, fun response -> reply.Reply(response)))
        }

    interface IDisposable with
        member __.Dispose() = port.Close()

Commands Serialization

Now that we can easily send bytes over the serial port, we need to serialize commands.

the format is :

1: 
2: 
3: 
4: 
5: 
0 - ushort : bytes size
2 - ushort : sequence number
4 - byte   : command type (direct/system, reply/noreply)
5 - ushort : global size (size of return values)
7 ...      : commands bytes

It is possible to send several commands in a single call.

Command type

The command type is encoded on a single byte. bit 0 indicates whether it's a direct command (activating motors/sensors) or system command (uploading/downloading files). Bit 7 indicates whether we expect a reply. We'll not use system commands in this post.

1: 
2: 
3: 
4: 
5: 
module CommandType =
    let directReply = 0x00uy
    let directNoReply = 0x80uy
    let systemReply = 0x01uy
    let systemNoReply = 0x81uy

OpCode

The type of command is indicated by a 1 or 2 bytes opcode. Here is an enum of the different opcodes supported by Ev3:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
35: 
36: 
37: 
38: 
39: 
40: 
41: 
42: 
43: 
44: 
45: 
46: 
47: 
type Opcode =
    | UIRead_GetFirmware = 0x810a
    | UIWrite_LED = 0x821b
    | UIButton_Pressed = 0x8309
    | UIDraw_Update = 0x8400
    | UIDraw_Clean = 0x8401
    | UIDraw_Pixel = 0x8402
    | UIDraw_Line = 0x8403
    | UIDraw_Circle = 0x8404
    | UIDraw_Text = 0x8405
    | UIDraw_FillRect = 0x8409
    | UIDraw_Rect = 0x840a
    | UIDraw_InverseRect = 0x8410
    | UIDraw_SelectFont = 0x8411
    | UIDraw_Topline = 0x8412
    | UIDraw_FillWindow = 0x8413
    | UIDraw_DotLine = 0x8415
    | UIDraw_FillCircle = 0x8418
    | UIDraw_BmpFile = 0x841c
    | Sound_Break = 0x9400
    | Sound_Tone = 0x9401
    | Sound_Play = 0x9402
    | Sound_Repeat = 0x9403
    | Sound_Service = 0x9404
    | InputDevice_GetTypeMode = 0x9905
    | InputDevice_GetDeviceName = 0x9915
    | InputDevice_GetModeName = 0x9916
    | InputDevice_ReadyPct = 0x991b
    | InputDevice_ReadyRaw = 0x991c
    | InputDevice_ReadySI = 0x991d
    | InputDevice_ClearAll = 0x990a
    | InputDevice_ClearChanges = 0x991a
    | InputRead = 0x9a
    | InputReadExt = 0x9e
    | InputReadSI = 0x9d
    | OutputStop = 0xa3
    | OutputPower = 0xa4
    | OutputSpeed = 0xa5
    | OutputStart = 0xa6
    | OutputPolarity = 0xa7
    | OutputReady = 0xaa
    | OutputStepPower = 0xac
    | OutputTimePower = 0xad
    | OutputStepSpeed = 0xae
    | OutputTimeSpeed = 0xaf
    | OutputStepSync = 0xb0
    | OutputTimeSync = 0xb1

and the list of system opcodes (not used)

1: 
2: 
3: 
4: 
5: 
6: 
type SystemOpcode =
    | BeginDownload = 0x92
    | ContinueDownload = 0x93
    | CloseFileHandle = 0x98
    | CreateDirectory = 0x9b
    | DeleteFile = 0x9c

To help with the serialization in a Span we define some inlines helpers:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
module Buffer =
    let inline write value (buffer: Span<byte>) =
        buffer.[0] <- value
        buffer.Slice(1)

    let inline writeUint16 (value:uint16) (buffer: Span<byte>) =
        BinaryPrimitives.WriteUInt16LittleEndian(buffer, value)
        buffer.Slice(2)

    let inline writeUint16BE (value:uint16) (buffer: Span<byte>) =
        BinaryPrimitives.WriteUInt16BigEndian(buffer, value)
        buffer.Slice(2)

    let inline writeUInt32 (value:uint32) (buffer:Span<byte>) =
        BinaryPrimitives.WriteUInt32LittleEndian(buffer, value)
        buffer.Slice(4)

    let inline writeString (s: string) (buffer: Span<byte>) =
        let len = Encoding.UTF8.GetBytes(s.AsSpan(), buffer)
        buffer.Slice(len)

Each of these functions write a value at the beginning of the span and slice the appropriate size to return a new span starting just after the value that was just written.

Using this was can write the serializeOpcode function:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
let serializeOpcode op buffer =
    if op > enum 0xff then
        // this is a 2 bytes opcode
        // it is serialized in big endian format
        Buffer.writeUint16BE (uint16 op) buffer
    else
        // this is a 1 byte opode
        Buffer.write (byte op) buffer

In order to allocate the buffer, we need to compute the total size. The following functions returns the size for an opcode:

1: 
2: 
let opcodeLength code =
    if code > enum 0xff then 2 else 1

Parameters

Each opcode is followed by a given number of parameters containing the mandatory values for the operation.

We define a union type to represents different parameter types and their values:

1: 
2: 
3: 
4: 
5: 
6: 
type Parameter =
    | Byte of uint8
    | UShort of uint16
    | UInt of uint32
    | String of string
    | GlobalIndex of uint8

All types are pretty explicit except the last one. GlobalIndex is used to indicate a byte offset in a response data. We'll use it later.

Each argument is composed of a 1 byte prefix indicating it's type/size, followed by the value itself. Here are the different prefixes:

1: 
2: 
3: 
4: 
5: 
6: 
module ParamSize =
    let byte = 0x81uy
    let short = 0x82uy
    let int = 0x83uy
    let string = 0x84uy
    let globalIndex = 0xe1uy

Now we can serialize a parameter and get its length:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
let serializeParam p (buffer: Span<byte>) =
    match p with
    | Byte v ->
        let b = Buffer.write ParamSize.byte buffer
        Buffer.write v b
    | UShort v ->
        let b = Buffer.write ParamSize.short buffer
        Buffer.writeUint16 v b
    | UInt v ->
        let b = Buffer.write ParamSize.int buffer
        Buffer.writeUInt32 v b
    | String s ->
        let b = Buffer.write ParamSize.string buffer
        let b =  Buffer.writeString s b
        Buffer.write 0uy b
    | GlobalIndex v -> 
        let b = Buffer.write ParamSize.globalIndex buffer
        Buffer.write v b

let paramLength = function
    | Byte _ | GlobalIndex _ -> 2
    | UShort _ -> 3
    | UInt _ -> 5
    | String l -> Encoding.UTF8.GetByteCount l + 2

So we can define a command as a union:

1: 
2: 
3: 
4: 
type Command =
    | Direct of Opcode * Parameter list
    // we will not model SystemCommand here
    // | SystemCommand of SystemOpcode * Parameter list

Computing the size of a command is fairly straightforward:

1: 
2: 
3: 
4: 
let length =
    function
    | Direct(code, parameters) ->
        opcodeLength code + List.sumBy paramLength parameters 

The serialization is a bit more complicated if we want to loop on parameters using a recursive function. The reason is that F# doesn't currently support Span<T> -> Span<T> function parameters. This signature is used by our serializers that take a Span, write at the begining an returns a shorter span starting after writen bytes. To wrokaround this problem we have to use a plain delegate:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
type Serializer<'t> = delegate of 't * Span<byte> -> Span<byte>

let rec serializeAll (f: 't Serializer) (ps: 't list) (b: Span<byte>) =
    match ps with
    | [] -> b
    | p :: t ->
        let b' = f.Invoke(p, b)
        serializeAll f t b'

The serializeAll function takes a Serializer, a list of values and a buffer. It serializes all elements in the list using the serializer and returns a span that starts just after the bytes we just wrote.

We use it to implement serializeCommand:

1: 
2: 
3: 
4: 
5: 
let serializeCommand command (buffer: Span<byte>) =
    match command with
    | Direct (op, p) ->
        let b = serializeOpcode op buffer
        serializeAll (Serializer serializeParam) p b

Putting it all together

We can now proceed to the serialize function that allocates memory from the MemoryPool, writes each part using the functions we defined above, and returns a memory owner. The memory owner has to be disposed to return the memory to the pool.

It takes a command list and serialize all the commands using serializeAll:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
let serialize sequence commandType globalSize commands =
    let length = 5 + List.sumBy length commands // there is a 5 bytes header
    let rental = MemoryPool.Shared.Rent(length+2) // plus the 2 bytes for size
    let mem = rental.Memory.Slice(0,length+2)
    

    let buffer = mem.Span
    let b = Buffer.writeUint16 (uint16 length) buffer // 2 bytes size
    let b = Buffer.writeUint16 sequence b   // 2 bytes sequence number
    let b = Buffer.write commandType b      // 1 byte command type
    let b = Buffer.writeUint16 globalSize b // 2 bytes globalSize of response
    let _ = serializeAll (Serializer serializeCommand) commands b // commands bytes
    { new IMemoryOwner<byte> with 
        member _.Memory = mem
        member _.Dispose() = rental.Dispose() }

And we write a send function that takes a list of commands, serialize them and sends the result to the brick:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
let send commands =
    fun (brick: Brick) ->
        async {
            // generates a sequence number
            let sequence = brick.GetNextSequence()
            // serialize the commands in a Memory buffer
            // (that will be disposed at the end of the call)
            use memory =
                commands
                |> serialize sequence CommandType.directNoReply 0us
      
            // send it to the brick
            do! brick.AsyncWrite(Memory.op_Implicit memory.Memory)
        }

Defining commands

We're now ready to define commands in a more friendly way.

For instance the startMotor command can be called with a list of output ports (the ones on the top of the brick calles A, B, C and D):

1: 
2: 
3: 
4: 
5: 
type OutputPort =
    | A 
    | B
    | C
    | D

Ports are encoded as a Byte parameter with bit 0 set for A, 1 for B, 2 for C, and 3 for D:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
let port p =
    p
    |> List.fold (fun v p ->
        v ||| match p with
              | A -> 0x01uy
              | B -> 0x02uy
              | C -> 0x04uy
              | D -> 0x08uy) 0uy
    |> Byte

startMotor is using the OutputStart opcode followed by two parameters: a 0 byte , and a port byte.

1: 
2: 
3: 
let startMotor ports =
    Direct(Opcode.OutputStart,
            [Byte 0uy; port ports])

stopMotor is using the OutputStop opcode followed by the same parameters, and an extra Break value encoded as a 0 or 1 byte:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
type Brake =
    | Brake
    | NoBrake
    with
    static member toByte b =
        match b with
        | Brake -> 0x01uy
        | NoBrake -> 0x00uy
        |> Byte 

let stopMotor ports brake = 
    Direct(Opcode.OutputStop, 
            [Byte 0uy; port ports; Brake.toByte brake ])

Here are a few more commands:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
type Power = Power of uint8
let power p =
    if p < -100 || p > 100 then
        invalidArg "p" "Power should be between -100 and 100"
    Power (uint8 p)

/// wait for the end of prvious command
let outputReady ports =
    Direct(Opcode.OutputReady,
            [ Byte 0uy; port ports;])

let turnMotorAtPower ports (Power power) =
    Direct(Opcode.OutputPower,
            [Byte 0uy; port ports; Byte power])

let turnMotorAtSpeedForTime' ports speed msRampUp msConstant msRampDown brake =
    Direct(Opcode.OutputTimeSpeed,
            [Byte 0uy
             port ports
             Byte (byte speed)
             UInt msRampUp
             UInt msConstant
             UInt msRampDown
             Brake.toByte brake])

let turnMotorAtSpeedForTime ports speed msDuration brake =
    turnMotorAtSpeedForTime' ports speed 0u msDuration 0u brake

// yes, you can also play musique :)
let playTone volume frequency duration =
    Direct(Opcode.Sound_Tone,
        [Byte volume
         UShort frequency
         UShort duration ])

You can try to put a few commands in the list and send them to the brick. It should work.

Computation Expression

To make it better, we continue with a lego computation expression that builds a full program taking commands and async local computations.

It is used to compose functions that takes a Brick and return an async:

1: 
type Lego<'a> = Brick -> Async<'a>

This is a classic Computation expression (not an applicative).

1: 
type LegoBuilder() =

The Bind method takes a Lego<'a> and a function that takes a 'a as an input to return a Lego<'b>. The result should be a Lego<'b>, so we write a lambda that takes a Brick.

We pass this brick to x, to get an Async<'a>, we pass the value from this async to f using async.Bind.

1: 
2: 
3: 
    member __.Bind(x : Lego<'a>, f : 'a -> Lego<'b> ) : Lego<'b> = 
        fun brick ->
            async.Bind(x brick, fun v -> f v brick)

This overload does quite the same but directly takes a Command list and send it

1: 
2: 
3: 
    member __.Bind(command : Command list, f : unit -> Lego<'b> ) : Lego<'b> = 
        fun brick ->
            async.Bind(send command brick, fun () -> f () brick)

Same thing for a single command

1: 
2: 
3: 
    member __.Bind(command : Command, f : unit -> Lego<'b> ) : Lego<'b> = 
        fun brick ->
            async.Bind(send [command] brick, fun () -> f () brick)

We can also do it for simple Async<'a> (for instance Async.Sleep(1000)). In this case we just bind it directly.

1: 
2: 
3: 
    member __.Bind(x : Async<'a>, f : 'a -> Lego<'b> ) : Lego<'b> = 
        fun brick ->
            async.Bind(x, fun v -> f v brick)

Return creates a Lego<'a> from a value. It just takes a brick that it will not use and returns an Async with the value.

1: 
    member __.Return x : Lego<'a> = fun _ -> async.Return x

Other methods implement return!, for, combination sequenced calls, etc.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
    // needed for return!
    member __.ReturnFrom x = x 

    // needed for for .. in .. do expressions
    member __.For<'T>(values : 'T seq, body: 'T -> Lego<unit>) = 
        fun brick ->
            async.For(values, fun t -> body t brick)

    // needed to have several do! combined
    member __.Combine(x, y) : Lego<'a>=
        fun ctx ->
            async.Combine(x ctx, y ctx)
    
    member __.Delay(f: unit -> Lego<'a>) =
        fun ctx ->
            async.Delay(fun () -> f () ctx)

    // needed for if without else
    member __.Zero() = fun ctx -> async.Zero()

    // needed for use
    member __.Using(d, f) = fun ctx ->
        async.Using(d, f ctx)

The run functions takes a Lego function, starts it asynchronously and returns a cancelation token to stop it.

1: 
2: 
3: 
4: 
let run brick (f: Lego<unit>) = 
    let cancelToken = new CancellationTokenSource()
    Async.Start(f brick, cancelToken.Token)
    cancelToken

The runSynchronously functions takes a Lego<'a> function, and runs it synchronously

1: 
2: 
let runSynchronously brick (f: Lego<'a>) = 
    Async.RunSynchronously(f brick)

We choose lego for our Computation Expression name

1: 
let lego = LegoBuilder()

Let's play

Here is a sample that connects to COM9 and repeat 3 times a batch of commands to turn motor in port A in one direction for 1 second, then in the other direction for another second. It stops motors once done.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
let sample() =
    use brick = new Brick("COM9")
    brick.Connect()

    lego {
            for i in 0..3 do
               do! [ turnMotorAtSpeedForTime [A] 50 1000u NoBrake
                     outputReady [A]
                     turnMotorAtSpeedForTime [A] -50 1000u NoBrake
                     outputReady [A] ] 
            do! stopMotor [A] NoBrake }
            |> runSynchronously brick

Sensors

The Ev3 comes with 3 default sensors, Color, Touch and IR. Extra gyroscopic and ultrasound sensors can be acquierd separatly.

Sensors are connected to input ports (1 to 4). Motor ports (A to D) can also be read to get the rotation of the motors:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
type InputPort =
    | In1
    | In2
    | In3
    | In4
    | InA
    | InB
    | InC
    | InD

/// gets the binary code for an input port
let inputPort = function 
                | In1 -> 0x00uy
                | In2 -> 0x01uy 
                | In3 -> 0x02uy
                | In4 -> 0x03uy
                | InA -> 0x10uy
                | InB -> 0x11uy
                | InC -> 0x12uy 
                | InD -> 0x13uy
                >> Byte

Sensors can return different values

  • Color, ambiant light, reflected light for the color sensor
  • Touch can return pressed/released state or bumps
  • IR can return proximity, and remote buttons state...

each of this mode correspond to a 1 byte value

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
type Mode =
    | TouchMode of TouchMode
    | ColorMode of ColorMode
    | IRMode of IRMode
and TouchMode = Touch | Bumps
and ColorMode = Reflective | Ambient | Color | ReflectiveRaw | ReflectiveRgb | Calibration
and IRMode = Proximity | Seek | Remote | RemoteA | SAlt | Calibrate

let modeToUInt8 = function
    | TouchMode Touch -> 0uy
    | TouchMode Bumps -> 1uy   
    | ColorMode Reflective -> 0uy
    | ColorMode Ambient -> 1uy
    | ColorMode Color -> 2uy
    | ColorMode ReflectiveRaw -> 3uy
    | ColorMode ReflectiveRgb -> 4uy
    | ColorMode Calibration -> 5uy
    | IRMode Proximity -> 0uy
    | IRMode Seek -> 1uy
    | IRMode Remote -> 2uy
    | IRMode RemoteA -> 3uy
    | IRMode SAlt -> 4uy
    | IRMode Calibrate -> 5uy

Different units are used depending on the sensor value:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
type ReadDataType =
    | SI            // a 4 bytes single (floating) value
    | Raw           // a 4 bytes integer
    | Percent       // a 1 byte percent
    | RGB           // 3 2 bytes integers

/// gets the byte length for each data type
let readDataTypeLen = function
    | ReadDataType.SI -> 4
    | ReadDataType.Raw -> 4
    | ReadDataType.Percent -> 1
    | ReadDataType.RGB -> 6

Read command

Each of the datatypes correspond to a differents read opcode:

1: 
2: 
3: 
4: 
5: 
let readOpcode = function
    | ReadDataType.SI -> Opcode.InputDevice_ReadySI
    | ReadDataType.Raw -> Opcode.InputDevice_ReadyRaw
    | ReadDataType.RGB -> Opcode.InputDevice_ReadyRaw // rgb is a specifiv Raw read
    | ReadDataType.Percent -> Opcode.InputDevice_ReadyPct

With this opcode we can create a read command as we did previously. The position parameter is the position of the corresponding value in the reponse bytes.

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
let readCommand (inPort, dataType, mode) position =
    Direct(readOpcode dataType ,
            [Byte 0uy
             inputPort inPort
             Byte 0uy
             Byte (modeToUInt8 mode)
             Byte 1uy
             GlobalIndex (uint8 position)])

For a single sensor request, it's easy, just pass 0 for position, and read the value at the start of the response.

The send function defined before was not waiting for a response. We create a request function the same way, but asks for a reply and use AsyncRequest to get a response. It checks the reply type (a byte just after response length).

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
type ReplyType =
| DirectReply = 0x02
| SystemReply = 0x03
| DirectReplyError = 0x04
| SystemReplyError = 0x05

let request commands globalSize =
    fun (brick: Brick) ->
        async {
            let sequence = brick.GetNextSequence()
            use data =
                commands
                |> serialize sequence CommandType.directReply globalSize

            let! response = brick.AsyncRequest(sequence, Memory.op_Implicit data.Memory)
            let replyType = enum<ReplyType> (int response.Span.[2])
            if replyType = ReplyType.DirectReplyError || replyType = ReplyType.SystemReplyError then
                failwith "An error occured"
            return response
        }

To query multiple sensors at once, we can create multiple read commands with different positions, and send them in a single batch. It avoids multiple roundtrip through bluetooth which can be expensive. The problem is that setting response value position can be error prone.

With the readDataTypeLen we can know the size of the value, and accumulate them to compute the offset. A readLength function that extracts the length from the triplet will make our life easier:

1: 
let readLength (_,dataType,_) = readDataTypeLen dataType

We accumulate the sizes as offsets using a mapFold. The accumulator in mapFold takes a state (the offset) and an input (the sensor paramaters), and outputs a value (the the command built using the offset) and a new state (the new offset computed by adding the value length).

This way we get a list of command with the rights offsets as well as the total length passed as globalSize.

To interpret the result, we use mapFold again. Now the state is a ReadOnlyMemory<byte> starting at the 3rd byte (to skip the size and response status, the reason for the Slice(3)). For each item, we compute its length. The item is a pair of the sensors parameter and a slice containing the value. The state is sliced to skip the bytes from current value and start at the next one.

We combine it with a call to request to have this function that takes a brick and a list of sensor parameters and returns a map of sensor parameters and their value as a ReadOnlyMemory<byte>.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
let readAux brick inputs =
    async {
        if List.isEmpty inputs then
            return Map.empty
        else
            let commands, globalSize =
                inputs
                |> List.mapFold (fun offset input -> 
                    readCommand input offset, (offset + readLength input)
                    ) 
                    0

            let! data = 
                request commands (uint16 globalSize) brick

            let sensorsData = data.Slice(3)
            let response =
                inputs
                |> List.mapFold (fun (data: ReadOnlyMemory<byte>) input ->
                    let length = readLength input 
                    (input, data.Slice(0, length)),
                    data.Slice(length))
                    sensorsData
                        
                            
                |> fst
                |> Map.ofList
            return response
        }

Applicative

We solved the problem of values offsets, but a few challenge remain. First the result is still in the form of a ReadOnlyMemory<byte>, and we should be carefull to read it accordingly to the requested data. Then we have to be sure we don't try to look for values in the result that we did not request.

If you've read previous posts already, you'll recognize the Query<'t> applicative. Here we will create a Sensor<'t> type:

1: 
2: 
3: 
4: 
5: 
type InputRequest = InputPort * ReadDataType * Mode

type Sensor<'t> =
    { Inputs: InputRequest Set 
      Get: Map<InputRequest, ReadOnlyMemory<byte>> -> 't }

It contains a set of inputs to request, and a function to extract a value of type 't from the result of our readAux function.

For instance the simplest way to create a sensor from an input:

1: 
2: 
3: 
let input (req: InputRequest) =
    { Inputs = Set.singleton req 
      Get = fun m -> m.[req] }

It indicates that it should request the given input and gets it from the map. The result will be a ReadOnlyMemory<byte>.

Creating a read functions that takes a Sensor<'t> and calls readAux is straightforwad:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
let read (sensor: Sensor<'t>)  =
    fun brick ->
        async {
            let inputs = Set.toList sensor.Inputs 
            // gets the Map response for requested inputs
            let! response = readAux brick inputs 
            // use the sensor.Get to extract value from response
            return sensor.Get response
        }

Using a Sensor<'t> and read, no way to get it wrong.

Now we want to create more complex sensors by combining simple ones.

Ret

We can create a pure sensor, a sensor that request no inputs and return a given value:

1: 
2: 
3: 
let ret x =
    { Inputs = Set.empty 
      Get = fun _ -> x }

It will be useful later.

Map

We can define a map operation on it (yes, it's a functor). It takes a 'a -> 'b functions and changes a Sensor<'a> to a Sensor<'b>:

1: 
2: 
3: 
let map (f: 'a -> 'b) (s: Sensor<'a>) : Sensor<'b> =
    { Inputs = s.Inputs 
      Get = fun m -> s.Get m |> f }

It requests the same inputs at the given sensor, gets its value and pass it to f.

Map2

More interesting, we can define a map2 that takes to sensors, and pass their values to a function to compute a single result. This result will also be a sensor that request for inputs of both, and combine their results using f:

1: 
2: 
3: 
4: 
5: 
6: 
let map2 f sx sy =
    { Inputs = sx.Inputs + sy.Inputs  // union of inputs
      Get = fun m ->
        let x = sx.Get m    // get value for sensor sx
        let y = sy.Get m    // get value for sensor sy
        f x y }             // combine the values using f

We can use map2 to zip sensors. It takes to sensors and create a single sensor that contains boths values as a tuple:

1: 
let zip sx sy = map2 (fun x y -> x,y) sx sy

Apply

The problem with map2, is that it works only with 2 sensors. For 3 sensors we would have to create a map3. And a map4, map5 etc.

But in F#, every function is a function of 1 argument due to currying. A function of type int -> bool -> string can be used as a int -> (bool -> string), it takes a single int argument and returns a function that takes a bool and returns a string.

Passing this function to map with a sensor of type Sensor<int>, we get a Sensor<bool -> string>. The meaning of this signature can seem obscure at first. It says that if you give this to the read function above, it will make a request and use the response values to build a bool -> string result. if you pass a bool to this result, you get a string.

But we would like the bool to also be read from a sensor. So we have a Sensor<bool -> string> and a Sensor<bool>. We can obviously use map2 on this pair of sensors. map2 will pass the values of the sensors to the function passed as an argument. The first argument of this function will be a bool -> string, the second a bool. We can pass the bool value to the bool -> string function to get a string. The result will be a Sensor<string>.

I used specific types to make it simpler by it works for any type:

1: 
2: 
let apply (sf: Sensor<'a -> 'b>) (sx: Sensor<'a>) : Sensor<'b> =
    map2 (fun f x -> f x) sf sx

Typed basic sensors

For a given data type, the conversion should always be the same. We create a function for each data type.

Each of the inputXXX functions take a port and a mode are returns a typed sensor.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
let typedInput dataType convert =
    fun port mode ->
        input (port, dataType, mode) |> map convert

        
let inputSi = typedInput ReadDataType.SI (fun data -> BitConverter.ToSingle(data.Span))
let inputRaw = typedInput ReadDataType.Raw (fun data -> BitConverter.ToInt32(data.Span))
let inputPct = typedInput ReadDataType.Percent (fun data -> int data.Span.[0])
let inputRgb = typedInput ReadDataType.RGB (fun data -> 
    let span = data.Span
    let r = BitConverter.ToUInt16(span)
    let g = BitConverter.ToUInt16(span.Slice(2))
    let b = BitConverter.ToUInt16(span.Slice(4))
    int r, int g, int b
    )

Using this functions we define more interesting sensors

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
35: 
36: 
37: 
38: 
39: 
40: 
41: 
42: 
43: 
44: 
45: 
46: 
47: 
48: 
// the state of the touch sensor
type ButtonState =
    | Pushed
    | Released

// the colors of the color sensor
type Color =
    | Transparent
    | Black
    | Blue
    | Green
    | Yellow
    | Red
    | White
    | Brown

module Sensors =
    module Color =
        let reflective port = inputPct port (ColorMode ColorMode.Reflective)
        let ambient port = inputPct port (ColorMode ColorMode.Ambient)

        // here we use the the SI single result
        // and map it to a Color
        let color port =
            inputSi port (ColorMode ColorMode.Color)
            |> map (function
                    | 1.f -> Black
                    | 2.f -> Blue
                    | 3.f -> Green
                    | 4.f -> Yellow
                    | 5.f -> Red
                    | 6.f -> White
                    | 7.f -> Brown
                    | _ -> Transparent)
        module Raw =
            let reflective port = inputRaw port (ColorMode ColorMode.ReflectiveRaw)
            // this one use a RGB type, the result is read as a RGB triplet
            let rgb port = inputRgb port (ColorMode ColorMode.ReflectiveRgb)


    module IR =
        let proximity port = inputPct port (IRMode Proximity)

    module Touch =
        // the SI single result is mapped to Pushed/Released
        let button port = 
            inputSi port (TouchMode Touch)
            |> map (function 1.f -> Pushed | _ -> Released)

Operators

Prior to F# 5.0 it is not possible to implement applicatives with computation expressions. Instead we can define <!> for map and <*> for apply to get infix syntax.

<!> takes a function on a left and a sensor on the right. We get a sensor with the argument applied. If it was a simple 1 argument function, we get a sensor of the result:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
let (<!>) = map
let (<*>) = apply


let isRed port : Sensor<bool> =
    (fun c -> c = Red) 
    <!> Sensors.Color.color port

If the function take several parameters, we get a sensor of a function as a result. We can use it with <*>, the operator for apply:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
let colorAndProximityIfPushed : Sensor<(Color * int) option> =
    (fun color proximity button ->
        match button with
        | Pushed -> Some (color, proximity)
        | Released -> None)
    <!> Sensors.Color.color In1
    <*> Sensors.IR.proximity In2
    <*> Sensors.Touch.button In3

This is a composit sensors that gets the state from the Touch sensor in input 3, color from Color sensor in input 1, and proximity from IR sensor on input 2. It returns None when the button is released, and Some(color, proximity) when the button is pushed.

Sensor Computation Expression

With F# 5.0 we can replace these operators by a computation expression.

We create a builder type as usual but implement BindReturn as map (with arguments swapped), and MergeSources as zip.

We can also provide a Bind2Return for the 2 parameter cases as map2.

The Return is implemented using ret, the pure value sensor:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
type SensorBuilder() =
    member _.BindReturn(x: 'a Sensor,f : 'a -> 'b) : 'b Sensor = map f x

    member _.MergeSources(x: 'a Sensor, y: 'b Sensor) : ('a * 'b) Sensor = zip x y

    member _.Bind2Return(x: 'a Sensor,y: 'b Sensor,f : 'a*'b -> 'c) : 'c Sensor = 
        map2 (fun x y -> f(x,y)) x y

    member _.Return x = ret x

let sensor = SensorBuilder()

Let's rewrite colorAndProximityIfPushed sensor with this computation expression:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
let colorWhenPushed =
    sensor {
        let! color = Sensors.Color.color In1
        and! proximity = Sensors.IR.proximity In2
        and! button = Sensors.Touch.button In3

        return match button with
               | Pushed -> Some (color, proximity)
               | Released -> None
    }

Notice the usage of and! to combine all the sensors together.

We put it all together in a final sample:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
let sample2() =
    use brick = new Brick("COM9")
    brick.Connect()

    lego {

        for i in 0 .. 100 do
            let! value = read (
                            sensor {
                                let! color = Sensors.Color.Raw.rgb In1
                                and! b = Sensors.Touch.button In2
                                and! prox = Sensors.IR.proximity In3

                                match b with
                                | Pushed -> return Some (color,prox)
                                | Released -> return None
                                    }
                                )
            printfn "%A" value

    } |> runSynchronously brick

Take your time to extend it to send any command and combine any sensors in a friendly and readable way.

The applicative computation expression for sensors provide a safe API over an error prone low level protocol. The syntax of computation expresions is also less obscure to newcomers than the <!> and <*> operators that can be a bit hard to explain.

Now you know what to ask Santa for Xmas ! Happy Xmas !

type Sequence = uint16
Multiple items
val uint16 : value:'T -> uint16 (requires member op_Explicit)

--------------------
type uint16 = System.UInt16
namespace System
namespace System.Threading
namespace System.Text
namespace System.Buffers
namespace System.Buffers.Binary
type Dispatch =
  | Request of sequence: Sequence * (ReadOnlyMemory<byte> -> unit)
  | Forward of sequence: Sequence * ReadOnlyMemory<byte>
union case Dispatch.Request: sequence: Sequence * (ReadOnlyMemory<byte> -> unit) -> Dispatch
Multiple items
type ReadOnlyMemory<'T> =
  struct
    new : array:'T[] -> ReadOnlyMemory<'T> + 1 overload
    member CopyTo : destination:Memory<'T> -> unit
    member Equals : obj:obj -> bool + 1 overload
    member GetHashCode : unit -> int
    member IsEmpty : bool
    member Length : int
    member Pin : unit -> MemoryHandle
    member Slice : start:int -> ReadOnlyMemory<'T> + 1 overload
    member Span : ReadOnlySpan<'T>
    member ToArray : unit -> 'T[]
    ...
  end

--------------------
ReadOnlyMemory ()
ReadOnlyMemory(array: 'T []) : ReadOnlyMemory<'T>
ReadOnlyMemory(array: 'T [], start: int, length: int) : ReadOnlyMemory<'T>
Multiple items
val byte : value:'T -> byte (requires member op_Explicit)

--------------------
type byte = Byte
type unit = Unit
union case Dispatch.Forward: sequence: Sequence * ReadOnlyMemory<byte> -> Dispatch
val responseDispatcher : unit -> MailboxProcessor<Dispatch>
Multiple items
type MailboxProcessor<'Msg> =
  interface IDisposable
  new : body:(MailboxProcessor<'Msg> -> Async<unit>) * ?cancellationToken:CancellationToken -> MailboxProcessor<'Msg>
  member Post : message:'Msg -> unit
  member PostAndAsyncReply : buildMessage:(AsyncReplyChannel<'Reply> -> 'Msg) * ?timeout:int -> Async<'Reply>
  member PostAndReply : buildMessage:(AsyncReplyChannel<'Reply> -> 'Msg) * ?timeout:int -> 'Reply
  member PostAndTryAsyncReply : buildMessage:(AsyncReplyChannel<'Reply> -> 'Msg) * ?timeout:int -> Async<'Reply option>
  member Receive : ?timeout:int -> Async<'Msg>
  member Scan : scanner:('Msg -> Async<'T> option) * ?timeout:int -> Async<'T>
  member Start : unit -> unit
  member TryPostAndReply : buildMessage:(AsyncReplyChannel<'Reply> -> 'Msg) * ?timeout:int -> 'Reply option
  ...

--------------------
new : body:(MailboxProcessor<'Msg> -> Async<unit>) * ?cancellationToken:CancellationToken -> MailboxProcessor<'Msg>
static member MailboxProcessor.Start : body:(MailboxProcessor<'Msg> -> Async<unit>) * ?cancellationToken:CancellationToken -> MailboxProcessor<'Msg>
val mailbox : MailboxProcessor<Dispatch>
val loop : (Map<Sequence,(ReadOnlyMemory<byte> -> unit)> -> Async<'a>)
val requests : Map<Sequence,(ReadOnlyMemory<byte> -> unit)>
val async : AsyncBuilder
val message : Dispatch
member MailboxProcessor.Receive : ?timeout:int -> Async<'Msg>
val newMap : Map<Sequence,(ReadOnlyMemory<byte> -> unit)>
val sequence : Sequence
val reply : (ReadOnlyMemory<byte> -> unit)
Multiple items
module Map

from Microsoft.FSharp.Collections

--------------------
type Map<'Key,'Value (requires comparison)> =
  interface IReadOnlyDictionary<'Key,'Value>
  interface IReadOnlyCollection<KeyValuePair<'Key,'Value>>
  interface IEnumerable
  interface IComparable
  interface IEnumerable<KeyValuePair<'Key,'Value>>
  interface ICollection<KeyValuePair<'Key,'Value>>
  interface IDictionary<'Key,'Value>
  new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
  member Add : key:'Key * value:'Value -> Map<'Key,'Value>
  member ContainsKey : key:'Key -> bool
  ...

--------------------
new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
val add : key:'Key -> value:'T -> table:Map<'Key,'T> -> Map<'Key,'T> (requires comparison)
val response : ReadOnlyMemory<byte>
val tryFind : key:'Key -> table:Map<'Key,'T> -> 'T option (requires comparison)
union case Option.Some: Value: 'T -> Option<'T>
val remove : key:'Key -> table:Map<'Key,'T> -> Map<'Key,'T> (requires comparison)
union case Option.None: Option<'T>
val empty<'Key,'T (requires comparison)> : Map<'Key,'T> (requires comparison)
Multiple items
type Brick =
  interface IDisposable
  new : name:string -> Brick
  member AsyncRequest : sequence:Sequence * data:ReadOnlyMemory<byte> -> Async<ReadOnlyMemory<byte>>
  member AsyncWrite : data:ReadOnlyMemory<byte> -> Async<unit>
  member Connect : unit -> unit
  member GetNextSequence : unit -> uint16

--------------------
new : name:string -> Brick
val name : string
val port : IO.Ports.SerialPort
namespace System.IO
namespace System.IO.Ports
Multiple items
type SerialPort =
  inherit Component
  new : unit -> SerialPort + 6 overloads
  member BaseStream : Stream
  member BaudRate : int with get, set
  member BreakState : bool with get, set
  member BytesToRead : int
  member BytesToWrite : int
  member CDHolding : bool
  member Close : unit -> unit
  member CtsHolding : bool
  member DataBits : int with get, set
  ...

--------------------
IO.Ports.SerialPort() : IO.Ports.SerialPort
IO.Ports.SerialPort(container: ComponentModel.IContainer) : IO.Ports.SerialPort
IO.Ports.SerialPort(portName: string) : IO.Ports.SerialPort
IO.Ports.SerialPort(portName: string, baudRate: int) : IO.Ports.SerialPort
IO.Ports.SerialPort(portName: string, baudRate: int, parity: IO.Ports.Parity) : IO.Ports.SerialPort
IO.Ports.SerialPort(portName: string, baudRate: int, parity: IO.Ports.Parity, dataBits: int) : IO.Ports.SerialPort
IO.Ports.SerialPort(portName: string, baudRate: int, parity: IO.Ports.Parity, dataBits: int, stopBits: IO.Ports.StopBits) : IO.Ports.SerialPort
val dispatcher : MailboxProcessor<Dispatch>
val mutable sequence : int
IO.Ports.SerialPort.Open() : unit
val reader : IO.BinaryReader
Multiple items
type BinaryReader =
  new : input:Stream -> BinaryReader + 2 overloads
  member BaseStream : Stream
  member Close : unit -> unit
  member Dispose : unit -> unit
  member PeekChar : unit -> int
  member Read : unit -> int + 4 overloads
  member ReadBoolean : unit -> bool
  member ReadByte : unit -> byte
  member ReadBytes : count:int -> byte[]
  member ReadChar : unit -> char
  ...

--------------------
IO.BinaryReader(input: IO.Stream) : IO.BinaryReader
IO.BinaryReader(input: IO.Stream, encoding: Encoding) : IO.BinaryReader
IO.BinaryReader(input: IO.Stream, encoding: Encoding, leaveOpen: bool) : IO.BinaryReader
property IO.Ports.SerialPort.BaseStream: IO.Stream with get
event IO.Ports.SerialPort.DataReceived: IEvent<IO.Ports.SerialDataReceivedEventHandler,IO.Ports.SerialDataReceivedEventArgs>
Multiple items
module Event

from Microsoft.FSharp.Control

--------------------
type Event<'T> =
  new : unit -> Event<'T>
  member Trigger : arg:'T -> unit
  member Publish : IEvent<'T>

--------------------
type Event<'Delegate,'Args (requires delegate and 'Delegate :> Delegate)> =
  new : unit -> Event<'Delegate,'Args>
  member Trigger : sender:obj * args:'Args -> unit
  member Publish : IEvent<'Delegate,'Args>

--------------------
new : unit -> Event<'T>

--------------------
new : unit -> Event<'Delegate,'Args>
val add : callback:('T -> unit) -> sourceEvent:IEvent<'Del,'T> -> unit (requires delegate and 'Del :> Delegate)
val e : IO.Ports.SerialDataReceivedEventArgs
property IO.Ports.SerialDataReceivedEventArgs.EventType: IO.Ports.SerialData with get, set
type SerialData =
  | Chars = 1
  | Eof = 2
field IO.Ports.SerialData.Chars: IO.Ports.SerialData = 1
val size : int16
IO.BinaryReader.ReadInt16() : int16
val response : byte []
IO.BinaryReader.ReadBytes(count: int) : byte []
Multiple items
val int : value:'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
val sequence : uint16
type BitConverter =
  static val IsLittleEndian : bool
  static member DoubleToInt64Bits : value:float -> int64
  static member GetBytes : value:bool -> byte[] + 9 overloads
  static member Int32BitsToSingle : value:int -> float32
  static member Int64BitsToDouble : value:int64 -> float
  static member SingleToInt32Bits : value:float32 -> int
  static member ToBoolean : value:ReadOnlySpan<byte> -> bool + 1 overload
  static member ToChar : value:ReadOnlySpan<byte> -> char + 1 overload
  static member ToDouble : value:ReadOnlySpan<byte> -> float + 1 overload
  static member ToInt16 : value:ReadOnlySpan<byte> -> int16 + 1 overload
  ...
BitConverter.ToUInt16(value: ReadOnlySpan<byte>) : uint16
BitConverter.ToUInt16(value: byte [], startIndex: int) : uint16
member MailboxProcessor.Post : message:'Msg -> unit
type Interlocked =
  static member Add : location1:int * value:int -> int + 1 overload
  static member CompareExchange : location1:int * value:int * comparand:int -> int + 6 overloads
  static member Decrement : location:int -> int + 1 overload
  static member Exchange : location1:int * value:int -> int + 6 overloads
  static member Increment : location:int -> int + 1 overload
  static member MemoryBarrier : unit -> unit
  static member MemoryBarrierProcessWide : unit -> unit
  static member Read : location:int64 -> int64
Interlocked.Increment(location: byref<int64>) : int64
Interlocked.Increment(location: byref<int>) : int
Multiple items
val uint16 : value:'T -> uint16 (requires member op_Explicit)

--------------------
type uint16 = UInt16
val data : ReadOnlyMemory<byte>
val task : Tasks.ValueTask
IO.Stream.WriteAsync(buffer: ReadOnlyMemory<byte>,?cancellationToken: CancellationToken) : Tasks.ValueTask
IO.Stream.WriteAsync(buffer: byte [], offset: int, count: int) : Tasks.Task
IO.Stream.WriteAsync(buffer: byte [], offset: int, count: int, cancellationToken: CancellationToken) : Tasks.Task
property Tasks.ValueTask.IsCompleted: bool with get
member AsyncBuilder.Return : value:'T -> Async<'T>
Multiple items
type Async =
  static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
  static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
  static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
  static member AwaitTask : task:Task -> Async<unit>
  static member AwaitTask : task:Task<'T> -> Async<'T>
  static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
  static member CancelDefaultToken : unit -> unit
  static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
  static member Choice : computations:seq<Async<'T option>> -> Async<'T option>
  static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
  ...

--------------------
type Async<'T> =
static member Async.AwaitTask : task:Tasks.Task -> Async<unit>
static member Async.AwaitTask : task:Tasks.Task<'T> -> Async<'T>
Tasks.ValueTask.AsTask() : Tasks.Task
val this : Brick
member Brick.AsyncWrite : data:ReadOnlyMemory<byte> -> Async<unit>
member MailboxProcessor.PostAndAsyncReply : buildMessage:(AsyncReplyChannel<'Reply> -> 'Msg) * ?timeout:int -> Async<'Reply>
val reply : AsyncReplyChannel<ReadOnlyMemory<byte>>
member AsyncReplyChannel.Reply : value:'Reply -> unit
type IDisposable =
  member Dispose : unit -> unit
IO.Ports.SerialPort.Close() : unit
Multiple items
val byte : value:'T -> byte (requires member op_Explicit)

--------------------
type byte = System.Byte
val directReply : byte
val directNoReply : byte
val systemReply : byte
val systemNoReply : byte
type Opcode =
  | UIRead_GetFirmware = 33034
  | UIWrite_LED = 33307
  | UIButton_Pressed = 33545
  | UIDraw_Update = 33792
  | UIDraw_Clean = 33793
  | UIDraw_Pixel = 33794
  | UIDraw_Line = 33795
  | UIDraw_Circle = 33796
  | UIDraw_Text = 33797
  | UIDraw_FillRect = 33801
  | UIDraw_Rect = 33802
  | UIDraw_InverseRect = 33808
  | UIDraw_SelectFont = 33809
  | UIDraw_Topline = 33810
  | UIDraw_FillWindow = 33811
  | UIDraw_DotLine = 33813
  | UIDraw_FillCircle = 33816
  | UIDraw_BmpFile = 33820
  | Sound_Break = 37888
  | Sound_Tone = 37889
  | Sound_Play = 37890
  | Sound_Repeat = 37891
  | Sound_Service = 37892
  | InputDevice_GetTypeMode = 39173
  | InputDevice_GetDeviceName = 39189
  | InputDevice_GetModeName = 39190
  | InputDevice_ReadyPct = 39195
  | InputDevice_ReadyRaw = 39196
  | InputDevice_ReadySI = 39197
  | InputDevice_ClearAll = 39178
  | InputDevice_ClearChanges = 39194
  | InputRead = 154
  | InputReadExt = 158
  | InputReadSI = 157
  | OutputStop = 163
  | OutputPower = 164
  | OutputSpeed = 165
  | OutputStart = 166
  | OutputPolarity = 167
  | OutputReady = 170
  | OutputStepPower = 172
  | OutputTimePower = 173
  | OutputStepSpeed = 174
  | OutputTimeSpeed = 175
  | OutputStepSync = 176
  | OutputTimeSync = 177
Opcode.UIRead_GetFirmware: Opcode = 33034
Opcode.UIWrite_LED: Opcode = 33307
Opcode.UIButton_Pressed: Opcode = 33545
Opcode.UIDraw_Update: Opcode = 33792
Opcode.UIDraw_Clean: Opcode = 33793
Opcode.UIDraw_Pixel: Opcode = 33794
Opcode.UIDraw_Line: Opcode = 33795
Opcode.UIDraw_Circle: Opcode = 33796
Opcode.UIDraw_Text: Opcode = 33797
Opcode.UIDraw_FillRect: Opcode = 33801
Opcode.UIDraw_Rect: Opcode = 33802
Opcode.UIDraw_InverseRect: Opcode = 33808
Opcode.UIDraw_SelectFont: Opcode = 33809
Opcode.UIDraw_Topline: Opcode = 33810
Opcode.UIDraw_FillWindow: Opcode = 33811
Opcode.UIDraw_DotLine: Opcode = 33813
Opcode.UIDraw_FillCircle: Opcode = 33816
Opcode.UIDraw_BmpFile: Opcode = 33820
Opcode.Sound_Break: Opcode = 37888
Opcode.Sound_Tone: Opcode = 37889
Opcode.Sound_Play: Opcode = 37890
Opcode.Sound_Repeat: Opcode = 37891
Opcode.Sound_Service: Opcode = 37892
Opcode.InputDevice_GetTypeMode: Opcode = 39173
Opcode.InputDevice_GetDeviceName: Opcode = 39189
Opcode.InputDevice_GetModeName: Opcode = 39190
Opcode.InputDevice_ReadyPct: Opcode = 39195
Opcode.InputDevice_ReadyRaw: Opcode = 39196
Opcode.InputDevice_ReadySI: Opcode = 39197
Opcode.InputDevice_ClearAll: Opcode = 39178
Opcode.InputDevice_ClearChanges: Opcode = 39194
Opcode.InputRead: Opcode = 154
Opcode.InputReadExt: Opcode = 158
Opcode.InputReadSI: Opcode = 157
Opcode.OutputStop: Opcode = 163
Opcode.OutputPower: Opcode = 164
Opcode.OutputSpeed: Opcode = 165
Opcode.OutputStart: Opcode = 166
Opcode.OutputPolarity: Opcode = 167
Opcode.OutputReady: Opcode = 170
Opcode.OutputStepPower: Opcode = 172
Opcode.OutputTimePower: Opcode = 173
Opcode.OutputStepSpeed: Opcode = 174
Opcode.OutputTimeSpeed: Opcode = 175
Opcode.OutputStepSync: Opcode = 176
Opcode.OutputTimeSync: Opcode = 177
type SystemOpcode =
  | BeginDownload = 146
  | ContinueDownload = 147
  | CloseFileHandle = 152
  | CreateDirectory = 155
  | DeleteFile = 156
SystemOpcode.BeginDownload: SystemOpcode = 146
SystemOpcode.ContinueDownload: SystemOpcode = 147
SystemOpcode.CloseFileHandle: SystemOpcode = 152
SystemOpcode.CreateDirectory: SystemOpcode = 155
SystemOpcode.DeleteFile: SystemOpcode = 156
type Buffer =
  static member BlockCopy : src:Array * srcOffset:int * dst:Array * dstOffset:int * count:int -> unit
  static member ByteLength : array:Array -> int
  static member GetByte : array:Array * index:int -> byte
  static member MemoryCopy : source:unit * destination:unit * destinationSizeInBytes:int64 * sourceBytesToCopy:int64 -> unit + 1 overload
  static member SetByte : array:Array * index:int * value:byte -> unit
val write : value:byte -> buffer:Span<byte> -> Span<byte>
val value : byte
val buffer : Span<byte>
Span.Slice(start: int) : Span<byte>
Span.Slice(start: int, length: int) : Span<byte>
val writeUint16 : value:uint16 -> buffer:Span<byte> -> Span<byte>
val value : uint16
type BinaryPrimitives =
  static member ReadInt16BigEndian : source:ReadOnlySpan<byte> -> int16
  static member ReadInt16LittleEndian : source:ReadOnlySpan<byte> -> int16
  static member ReadInt32BigEndian : source:ReadOnlySpan<byte> -> int
  static member ReadInt32LittleEndian : source:ReadOnlySpan<byte> -> int
  static member ReadInt64BigEndian : source:ReadOnlySpan<byte> -> int64
  static member ReadInt64LittleEndian : source:ReadOnlySpan<byte> -> int64
  static member ReadUInt16BigEndian : source:ReadOnlySpan<byte> -> uint16
  static member ReadUInt16LittleEndian : source:ReadOnlySpan<byte> -> uint16
  static member ReadUInt32BigEndian : source:ReadOnlySpan<byte> -> uint32
  static member ReadUInt32LittleEndian : source:ReadOnlySpan<byte> -> uint32
  ...
BinaryPrimitives.WriteUInt16LittleEndian(destination: Span<byte>, value: uint16) : unit
val writeUint16BE : value:uint16 -> buffer:Span<byte> -> Span<byte>
BinaryPrimitives.WriteUInt16BigEndian(destination: Span<byte>, value: uint16) : unit
val writeUInt32 : value:uint32 -> buffer:Span<byte> -> Span<byte>
val value : uint32
Multiple items
val uint32 : value:'T -> uint32 (requires member op_Explicit)

--------------------
type uint32 = UInt32
BinaryPrimitives.WriteUInt32LittleEndian(destination: Span<byte>, value: uint32) : unit
val writeString : s:string -> buffer:Span<byte> -> Span<byte>
val s : string
Multiple items
val string : value:'T -> string

--------------------
type string = String
val len : int
type Encoding =
  member BodyName : string
  member Clone : unit -> obj
  member CodePage : int
  member DecoderFallback : DecoderFallback with get, set
  member EncoderFallback : EncoderFallback with get, set
  member EncodingName : string
  member Equals : value:obj -> bool
  member GetByteCount : chars:char[] -> int + 5 overloads
  member GetBytes : chars:char[] -> byte[] + 7 overloads
  member GetCharCount : bytes:byte[] -> int + 3 overloads
  ...
property Encoding.UTF8: Encoding with get
Encoding.GetBytes(s: string) : byte []
Encoding.GetBytes(chars: char []) : byte []
Encoding.GetBytes(chars: ReadOnlySpan<char>, bytes: Span<byte>) : int
Encoding.GetBytes(s: string, index: int, count: int) : byte []
Encoding.GetBytes(chars: char [], index: int, count: int) : byte []
Encoding.GetBytes(chars: nativeptr<char>, charCount: int, bytes: nativeptr<byte>, byteCount: int) : int
Encoding.GetBytes(s: string, charIndex: int, charCount: int, bytes: byte [], byteIndex: int) : int
Encoding.GetBytes(chars: char [], charIndex: int, charCount: int, bytes: byte [], byteIndex: int) : int
(extension) String.AsSpan() : ReadOnlySpan<char>
(extension) String.AsSpan(start: int) : ReadOnlySpan<char>
(extension) String.AsSpan(start: int, length: int) : ReadOnlySpan<char>
val serializeOpcode : op:Opcode -> buffer:Span<byte> -> Span<byte>
val op : Opcode
val enum : value:int32 -> 'U (requires enum)
val opcodeLength : code:Opcode -> int
val code : Opcode
type Parameter =
  | Byte of uint8
  | UShort of uint16
  | UInt of uint32
  | String of string
  | GlobalIndex of uint8
Multiple items
union case Parameter.Byte: uint8 -> Parameter

--------------------
type Byte =
  struct
    member CompareTo : value:obj -> int + 1 overload
    member Equals : obj:obj -> bool + 1 overload
    member GetHashCode : unit -> int
    member GetTypeCode : unit -> TypeCode
    member ToString : unit -> string + 3 overloads
    member TryFormat : destination:Span<char> * charsWritten:int * ?format:ReadOnlySpan<char> * ?provider:IFormatProvider -> bool
    static val MaxValue : byte
    static val MinValue : byte
    static member Parse : s:string -> byte + 4 overloads
    static member TryParse : s:string * result:byte -> bool + 3 overloads
  end
Multiple items
val uint8 : value:'T -> uint8 (requires member op_Explicit)

--------------------
type uint8 = Byte
union case Parameter.UShort: uint16 -> Parameter
union case Parameter.UInt: uint32 -> Parameter
Multiple items
union case Parameter.String: string -> Parameter

--------------------
type String =
  new : value:char[] -> string + 8 overloads
  member Chars : int -> char
  member Clone : unit -> obj
  member CompareTo : value:obj -> int + 1 overload
  member Contains : value:string -> bool + 3 overloads
  member CopyTo : sourceIndex:int * destination:char[] * destinationIndex:int * count:int -> unit
  member EndsWith : value:string -> bool + 3 overloads
  member EnumerateRunes : unit -> StringRuneEnumerator
  member Equals : obj:obj -> bool + 2 overloads
  member GetEnumerator : unit -> CharEnumerator
  ...

--------------------
String(value: char []) : String
String(value: nativeptr<char>) : String
String(value: nativeptr<sbyte>) : String
String(value: ReadOnlySpan<char>) : String
String(c: char, count: int) : String
String(value: char [], startIndex: int, length: int) : String
String(value: nativeptr<char>, startIndex: int, length: int) : String
String(value: nativeptr<sbyte>, startIndex: int, length: int) : String
String(value: nativeptr<sbyte>, startIndex: int, length: int, enc: Encoding) : String
union case Parameter.GlobalIndex: uint8 -> Parameter
Multiple items
val byte : byte

--------------------
type byte = Byte
val short : byte
Multiple items
val int : byte

--------------------
type int = int32

--------------------
type int<'Measure> = int
Multiple items
val string : byte

--------------------
type string = String
val globalIndex : byte
val serializeParam : p:Parameter -> buffer:Span<byte> -> Span<byte>
val p : Parameter
val v : uint8
val b : Span<byte>
module ParamSize

from 2020-12-03-applicative-computation-expressions-3
val byte : byte
val v : uint16
val v : uint32
val int : byte
val string : byte
val paramLength : _arg1:Parameter -> int
val l : string
Encoding.GetByteCount(chars: ReadOnlySpan<char>) : int
Encoding.GetByteCount(s: string) : int
Encoding.GetByteCount(chars: char []) : int
Encoding.GetByteCount(chars: nativeptr<char>, count: int) : int
Encoding.GetByteCount(s: string, index: int, count: int) : int
Encoding.GetByteCount(chars: char [], index: int, count: int) : int
type Command = | Direct of Opcode * Parameter list
union case Command.Direct: Opcode * Parameter list -> Command
type 'T list = List<'T>
val length : _arg1:Command -> int
val parameters : Parameter list
Multiple items
module List

from Microsoft.FSharp.Collections

--------------------
type List<'T> =
  | ( [] )
  | ( :: ) of Head: 'T * Tail: 'T list
    interface IReadOnlyList<'T>
    interface IReadOnlyCollection<'T>
    interface IEnumerable
    interface IEnumerable<'T>
    member GetReverseIndex : rank:int * offset:int -> int
    member GetSlice : startIndex:int option * endIndex:int option -> 'T list
    member Head : 'T
    member IsEmpty : bool
    member Item : index:int -> 'T with get
    member Length : int
    ...
val sumBy : projection:('T -> 'U) -> list:'T list -> 'U (requires member ( + ) and member get_Zero)
type Serializer<'t> =
  delegate of 't * Span<byte> -> Span<byte>
val serializeAll : f:Serializer<'t> -> ps:'t list -> b:Span<byte> -> Span<byte>
val f : Serializer<'t>
val ps : 't list
val p : 't
val t : 't list
val b' : Span<byte>
abstract member Serializer.Invoke : 't * Span<byte> -> Span<byte>
val serializeCommand : command:Command -> buffer:Span<byte> -> Span<byte>
val command : Command
val p : Parameter list
val serialize : sequence:uint16 -> commandType:byte -> globalSize:uint16 -> commands:Command list -> IMemoryOwner<byte>
val commandType : byte
val globalSize : uint16
val commands : Command list
val length : int
val rental : IMemoryOwner<byte>
type MemoryPool<'T> =
  member Dispose : unit -> unit
  member MaxBufferSize : int
  member Rent : ?minBufferSize:int -> IMemoryOwner<'T>
  static member Shared : MemoryPool<'T>
property MemoryPool.Shared: MemoryPool<'T> with get
MemoryPool.Rent(?minBufferSize: int) : IMemoryOwner<'T>
val mem : Memory<byte>
property IMemoryOwner.Memory: Memory<byte> with get
Memory.Slice(start: int) : Memory<byte>
Memory.Slice(start: int, length: int) : Memory<byte>
property Memory.Span: Span<byte> with get
type IMemoryOwner<'T> =
  inherit IDisposable
  member Memory : Memory<'T>
Multiple items
type Memory<'T> =
  struct
    new : array:'T[] -> Memory<'T> + 1 overload
    member CopyTo : destination:Memory<'T> -> unit
    member Equals : obj:obj -> bool + 1 overload
    member GetHashCode : unit -> int
    member IsEmpty : bool
    member Length : int
    member Pin : unit -> MemoryHandle
    member Slice : start:int -> Memory<'T> + 1 overload
    member Span : Span<'T>
    member ToArray : unit -> 'T[]
    ...
  end

--------------------
Memory ()
Memory(array: 'T []) : Memory<'T>
Memory(array: 'T [], start: int, length: int) : Memory<'T>
IDisposable.Dispose() : unit
val send : commands:Command list -> brick:Brick -> Async<unit>
val brick : Brick
member Brick.GetNextSequence : unit -> uint16
val memory : IMemoryOwner<byte>
module CommandType

from 2020-12-03-applicative-computation-expressions-3
Memory.op_Implicit(memory: Memory<'T>) : ReadOnlyMemory<'T>
Memory.op_Implicit(segment: ArraySegment<'T>) : Memory<'T>
Memory.op_Implicit(array: 'T []) : Memory<'T>
type OutputPort =
  | A
  | B
  | C
  | D
union case OutputPort.A: OutputPort
union case OutputPort.B: OutputPort
union case OutputPort.C: OutputPort
union case OutputPort.D: OutputPort
val port : p:OutputPort list -> Parameter
val p : OutputPort list
val fold : folder:('State -> 'T -> 'State) -> state:'State -> list:'T list -> 'State
val v : byte
val p : OutputPort
val startMotor : ports:OutputPort list -> Command
val ports : OutputPort list
Multiple items
union case Brake.Brake: Brake

--------------------
type Brake =
  | Brake
  | NoBrake
    static member toByte : b:Brake -> Parameter
union case Brake.NoBrake: Brake
val b : Brake
val stopMotor : ports:OutputPort list -> brake:Brake -> Command
val brake : Brake
static member Brake.toByte : b:Brake -> Parameter
Multiple items
union case Power.Power: uint8 -> Power

--------------------
type Power = | Power of uint8
val power : p:int -> Power
val p : int
val invalidArg : argumentName:string -> message:string -> 'T
val outputReady : ports:OutputPort list -> Command


 wait for the end of prvious command
val turnMotorAtPower : ports:OutputPort list -> Power -> Command
val power : uint8
val turnMotorAtSpeedForTime' : ports:OutputPort list -> speed:int -> msRampUp:uint32 -> msConstant:uint32 -> msRampDown:uint32 -> brake:Brake -> Command
val speed : int
val msRampUp : uint32
val msConstant : uint32
val msRampDown : uint32
val turnMotorAtSpeedForTime : ports:OutputPort list -> speed:int -> msDuration:uint32 -> brake:Brake -> Command
val msDuration : uint32
val playTone : volume:uint8 -> frequency:uint16 -> duration:uint16 -> Command
val volume : uint8
val frequency : uint16
val duration : uint16
type Lego<'a> = Brick -> Async<'a>
Multiple items
type LegoBuilder =
  new : unit -> LegoBuilder
  member Bind : x:Lego<'a> * f:('a -> Lego<'b>) -> Lego<'b>
  member Bind : command:Command list * f:(unit -> Lego<'b>) -> Lego<'b>
  member Bind : command:Command * f:(unit -> Lego<'b>) -> Lego<'b>
  member Bind : x:Async<'a> * f:('a -> Lego<'b>) -> Lego<'b>
  member Combine : x:(Brick -> Async<unit>) * y:(Brick -> Async<'a>) -> Lego<'a>
  member Delay : f:(unit -> Lego<'a>) -> (Brick -> Async<'a>)
  member For : values:seq<'T> * body:('T -> Lego<unit>) -> (Brick -> Async<unit>)
  member Return : x:'a -> Lego<'a>
  member ReturnFrom : x:'e -> 'e
  ...

--------------------
new : unit -> LegoBuilder
val x : Lego<'a>
val f : ('a -> Lego<'b>)
member AsyncBuilder.Bind : computation:Async<'T> * binder:('T -> Async<'U>) -> Async<'U>
val v : 'a
val __ : LegoBuilder
val command : Command list
val f : (unit -> Lego<'b>)
val x : Async<'a>
val x : 'a
val x : 'e
val values : seq<'T>
Multiple items
val seq : sequence:seq<'T> -> seq<'T>

--------------------
type seq<'T> = Collections.Generic.IEnumerable<'T>
val body : ('T -> Lego<unit>)
member AsyncBuilder.For : sequence:seq<'T> * body:('T -> Async<unit>) -> Async<unit>
val t : 'T
val x : (Brick -> Async<unit>)
val y : (Brick -> Async<'a>)
val ctx : Brick
member AsyncBuilder.Combine : computation1:Async<unit> * computation2:Async<'T> -> Async<'T>
val f : (unit -> Lego<'a>)
member AsyncBuilder.Delay : generator:(unit -> Async<'T>) -> Async<'T>
val ctx : 'd
member AsyncBuilder.Zero : unit -> Async<unit>
val d : #IDisposable
val f : ('b -> #IDisposable -> Async<'c>)
val ctx : 'b
member AsyncBuilder.Using : resource:'T * binder:('T -> Async<'U>) -> Async<'U> (requires 'T :> IDisposable)
val run : brick:Brick -> f:Lego<unit> -> CancellationTokenSource
val f : Lego<unit>
val cancelToken : CancellationTokenSource
Multiple items
type CancellationTokenSource =
  new : unit -> CancellationTokenSource + 2 overloads
  member Cancel : unit -> unit + 1 overload
  member CancelAfter : delay:TimeSpan -> unit + 1 overload
  member Dispose : unit -> unit
  member IsCancellationRequested : bool
  member Token : CancellationToken
  static member CreateLinkedTokenSource : [<ParamArray>] tokens:CancellationToken[] -> CancellationTokenSource + 1 overload

--------------------
CancellationTokenSource() : CancellationTokenSource
CancellationTokenSource(delay: TimeSpan) : CancellationTokenSource
CancellationTokenSource(millisecondsDelay: int) : CancellationTokenSource
static member Async.Start : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
property CancellationTokenSource.Token: CancellationToken with get
val runSynchronously : brick:Brick -> f:Lego<'a> -> 'a
val f : Lego<'a>
static member Async.RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:CancellationToken -> 'T
val lego : LegoBuilder
val sample : unit -> unit
member Brick.Connect : unit -> unit
val i : int
type InputPort =
  | In1
  | In2
  | In3
  | In4
  | InA
  | InB
  | InC
  | InD
union case InputPort.In1: InputPort
union case InputPort.In2: InputPort
union case InputPort.In3: InputPort
union case InputPort.In4: InputPort
union case InputPort.InA: InputPort
union case InputPort.InB: InputPort
union case InputPort.InC: InputPort
union case InputPort.InD: InputPort
val inputPort : (InputPort -> Parameter)


 gets the binary code for an input port
type Mode =
  | TouchMode of TouchMode
  | ColorMode of ColorMode
  | IRMode of IRMode
Multiple items
union case Mode.TouchMode: TouchMode -> Mode

--------------------
type TouchMode =
  | Touch
  | Bumps
Multiple items
union case Mode.ColorMode: ColorMode -> Mode

--------------------
type ColorMode =
  | Reflective
  | Ambient
  | Color
  | ReflectiveRaw
  | ReflectiveRgb
  | Calibration
Multiple items
union case Mode.IRMode: IRMode -> Mode

--------------------
type IRMode =
  | Proximity
  | Seek
  | Remote
  | RemoteA
  | SAlt
  | Calibrate
type TouchMode =
  | Touch
  | Bumps
union case TouchMode.Touch: TouchMode
union case TouchMode.Bumps: TouchMode
type ColorMode =
  | Reflective
  | Ambient
  | Color
  | ReflectiveRaw
  | ReflectiveRgb
  | Calibration
union case ColorMode.Reflective: ColorMode
union case ColorMode.Ambient: ColorMode
union case ColorMode.Color: ColorMode
union case ColorMode.ReflectiveRaw: ColorMode
union case ColorMode.ReflectiveRgb: ColorMode
union case ColorMode.Calibration: ColorMode
type IRMode =
  | Proximity
  | Seek
  | Remote
  | RemoteA
  | SAlt
  | Calibrate
union case IRMode.Proximity: IRMode
union case IRMode.Seek: IRMode
union case IRMode.Remote: IRMode
union case IRMode.RemoteA: IRMode
union case IRMode.SAlt: IRMode
union case IRMode.Calibrate: IRMode
val modeToUInt8 : _arg1:Mode -> byte
type ReadDataType =
  | SI
  | Raw
  | Percent
  | RGB
union case ReadDataType.SI: ReadDataType
union case ReadDataType.Raw: ReadDataType
union case ReadDataType.Percent: ReadDataType
union case ReadDataType.RGB: ReadDataType
val readDataTypeLen : _arg1:ReadDataType -> int


 gets the byte length for each data type
val readOpcode : _arg1:ReadDataType -> Opcode
val readCommand : inPort:InputPort * dataType:ReadDataType * mode:Mode -> position:int -> Command
val inPort : InputPort
val dataType : ReadDataType
val mode : Mode
val position : int
type ReplyType =
  | DirectReply = 2
  | SystemReply = 3
  | DirectReplyError = 4
  | SystemReplyError = 5
ReplyType.DirectReply: ReplyType = 2
ReplyType.SystemReply: ReplyType = 3
ReplyType.DirectReplyError: ReplyType = 4
ReplyType.SystemReplyError: ReplyType = 5
val request : commands:Command list -> globalSize:uint16 -> brick:Brick -> Async<ReadOnlyMemory<byte>>
val data : IMemoryOwner<byte>
member Brick.AsyncRequest : sequence:Sequence * data:ReadOnlyMemory<byte> -> Async<ReadOnlyMemory<byte>>
val replyType : ReplyType
property ReadOnlyMemory.Span: ReadOnlySpan<byte> with get
val failwith : message:string -> 'T
val readLength : 'a * dataType:ReadDataType * 'b -> int
val readAux : brick:Brick -> inputs:(InputPort * ReadDataType * Mode) list -> Async<Map<(InputPort * ReadDataType * Mode),ReadOnlyMemory<byte>>>
val inputs : (InputPort * ReadDataType * Mode) list
val isEmpty : list:'T list -> bool
val globalSize : int
val mapFold : mapping:('State -> 'T -> 'Result * 'State) -> state:'State -> list:'T list -> 'Result list * 'State
val offset : int
val input : InputPort * ReadDataType * Mode
val sensorsData : ReadOnlyMemory<byte>
ReadOnlyMemory.Slice(start: int) : ReadOnlyMemory<byte>
ReadOnlyMemory.Slice(start: int, length: int) : ReadOnlyMemory<byte>
val response : Map<(InputPort * ReadDataType * Mode),ReadOnlyMemory<byte>>
val fst : tuple:('T1 * 'T2) -> 'T1
val ofList : elements:('Key * 'T) list -> Map<'Key,'T> (requires comparison)
type InputRequest = InputPort * ReadDataType * Mode
type Sensor<'t> =
  { Inputs: Set<InputRequest>
    Get: Map<InputRequest,ReadOnlyMemory<byte>> -> 't }
Sensor.Inputs: Set<InputRequest>
Multiple items
module Set

from Microsoft.FSharp.Collections

--------------------
type Set<'T (requires comparison)> =
  interface IReadOnlyCollection<'T>
  interface IComparable
  interface IEnumerable
  interface IEnumerable<'T>
  interface ICollection<'T>
  new : elements:seq<'T> -> Set<'T>
  member Add : value:'T -> Set<'T>
  member Contains : value:'T -> bool
  override Equals : obj -> bool
  member IsProperSubsetOf : otherSet:Set<'T> -> bool
  ...

--------------------
new : elements:seq<'T> -> Set<'T>
Sensor.Get: Map<InputRequest,ReadOnlyMemory<byte>> -> 't
val input : InputPort * ReadDataType * Mode -> Sensor<ReadOnlyMemory<byte>>
val req : InputRequest
val singleton : value:'T -> Set<'T> (requires comparison)
val m : Map<InputRequest,ReadOnlyMemory<byte>>
val read : sensor:Sensor<'t> -> brick:Brick -> Async<'t>
val sensor : Sensor<'t>
val inputs : InputRequest list
val toList : set:Set<'T> -> 'T list (requires comparison)
val ret : x:'a -> Sensor<'a>
val empty<'T (requires comparison)> : Set<'T> (requires comparison)
val map : f:('a -> 'b) -> s:Sensor<'a> -> Sensor<'b>
val f : ('a -> 'b)
val s : Sensor<'a>
Sensor.Get: Map<InputRequest,ReadOnlyMemory<byte>> -> 'a
val map2 : f:('a -> 'b -> 'c) -> sx:Sensor<'a> -> sy:Sensor<'b> -> Sensor<'c>
val f : ('a -> 'b -> 'c)
val sx : Sensor<'a>
val sy : Sensor<'b>
val y : 'b
Sensor.Get: Map<InputRequest,ReadOnlyMemory<byte>> -> 'b
val zip : sx:Sensor<'a> -> sy:Sensor<'b> -> Sensor<'a * 'b>
val apply : sf:Sensor<('a -> 'b)> -> sx:Sensor<'a> -> Sensor<'b>
val sf : Sensor<('a -> 'b)>
val typedInput : dataType:ReadDataType -> convert:(ReadOnlyMemory<byte> -> 'a) -> port:InputPort -> mode:Mode -> Sensor<'a>
val convert : (ReadOnlyMemory<byte> -> 'a)
val port : InputPort
val inputSi : (InputPort -> Mode -> Sensor<float32>)
BitConverter.ToSingle(value: ReadOnlySpan<byte>) : float32
BitConverter.ToSingle(value: byte [], startIndex: int) : float32
val inputRaw : (InputPort -> Mode -> Sensor<int>)
BitConverter.ToInt32(value: ReadOnlySpan<byte>) : int
BitConverter.ToInt32(value: byte [], startIndex: int) : int
val inputPct : (InputPort -> Mode -> Sensor<int>)
val inputRgb : (InputPort -> Mode -> Sensor<int * int * int>)
val span : ReadOnlySpan<byte>
val r : uint16
val g : uint16
ReadOnlySpan.Slice(start: int) : ReadOnlySpan<byte>
ReadOnlySpan.Slice(start: int, length: int) : ReadOnlySpan<byte>
val b : uint16
type ButtonState =
  | Pushed
  | Released
union case ButtonState.Pushed: ButtonState
union case ButtonState.Released: ButtonState
Multiple items
union case ColorMode.Color: ColorMode

--------------------
type Color =
  | Transparent
  | Black
  | Blue
  | Green
  | Yellow
  | Red
  | White
  | Brown
union case Color.Transparent: Color
union case Color.Black: Color
union case Color.Blue: Color
union case Color.Green: Color
union case Color.Yellow: Color
union case Color.Red: Color
union case Color.White: Color
union case Color.Brown: Color
val reflective : port:InputPort -> Sensor<int>
val ambient : port:InputPort -> Sensor<int>
val color : port:InputPort -> Sensor<Color>
Multiple items
union case ReadDataType.Raw: ReadDataType

--------------------
module Raw

from 2020-12-03-applicative-computation-expressions-3.Sensors.Color
val rgb : port:InputPort -> Sensor<int * int * int>
val proximity : port:InputPort -> Sensor<int>
Multiple items
union case TouchMode.Touch: TouchMode

--------------------
module Touch

from 2020-12-03-applicative-computation-expressions-3.Sensors
val button : port:InputPort -> Sensor<ButtonState>
val isRed : port:InputPort -> Sensor<bool>
type bool = Boolean
val c : Color
module Sensors

from 2020-12-03-applicative-computation-expressions-3
module Color

from 2020-12-03-applicative-computation-expressions-3.Sensors
val colorAndProximityIfPushed : Sensor<(Color * int) option>
type 'T option = Option<'T>
val color : Color
val proximity : int
val button : ButtonState
module IR

from 2020-12-03-applicative-computation-expressions-3.Sensors
module Touch

from 2020-12-03-applicative-computation-expressions-3.Sensors
Multiple items
type SensorBuilder =
  new : unit -> SensorBuilder
  member Bind2Return : x:Sensor<'a> * y:Sensor<'b> * f:('a * 'b -> 'c) -> Sensor<'c>
  member BindReturn : x:Sensor<'a> * f:('a -> 'b) -> Sensor<'b>
  member MergeSources : x:Sensor<'a> * y:Sensor<'b> -> Sensor<'a * 'b>
  member Return : x:'a -> Sensor<'a>

--------------------
new : unit -> SensorBuilder
val x : Sensor<'a>
val y : Sensor<'b>
val f : ('a * 'b -> 'c)
val sensor : SensorBuilder
val colorWhenPushed : Sensor<(Color * int) option>
val sample2 : unit -> unit
val value : ((int * int * int) * int) option
val color : int * int * int
module Raw

from 2020-12-03-applicative-computation-expressions-3.Sensors.Color
val b : ButtonState
val prox : int
val printfn : format:Printf.TextWriterFormat<'T> -> 'T