ExSPIKE

Stateless encoder and decoder for the LEGO® SPIKE™ Prime BLE protocol, written in Elixir and based on the official protocol documentation.

Important

ExSPIKE is an independent project. It is not affiliated with, sponsored, authorized or endorsed by The LEGO Group. LEGO® and SPIKE™ are trademarks of The LEGO Group.

ExSPIKE turns messages into bytes and bytes into messages. It holds no state and starts no processes: you bring the BLE connection.

Installation

def deps do
[{:ex_spike, "~> 0.1.0"}]
end

Usage

Sending messages

Build a message and encode it into a frame ready to write to the hub's RX characteristic:

frame = ExSPIKE.Messages.info_request() |> ExSPIKE.encode()
#=> <<0, 0, 2>>
# Messages can also be written as raw bitstrings:
frame = ExSPIKE.encode(<<0x1E, 0x00, 0x05>>) # start program in slot 5

If a frame is larger than the hub's max_packet_size, split it:

ExSPIKE.Frame.packets(frame, info.max_packet_size)

Receiving messages

BLE notifications can hold part of a frame, or several frames. Pass the bytes to decode_stream/1 and keep the rest for the next notification:

{results, rest} = ExSPIKE.decode_stream(rest <> notification)
for {:ok, message} <- results do
case message do
%ExSPIKE.Message.ConsoleNotification{text: text} -> IO.puts(text)
%ExSPIKE.Message.DeviceNotification{messages: devices} -> IO.inspect(devices)
_ -> :ok
end
end

A single complete frame can be decoded with ExSPIKE.decode/1, and raw message bytes with ExSPIKE.Message.decode/1.

Uploading and running a program

program = "print('Hello from Elixir')"
[
ExSPIKE.Messages.clear_slot(0),
ExSPIKE.Messages.start_file_upload("program.py", 0, ExSPIKE.CRC.crc32(program))
| ExSPIKE.Messages.transfer_chunks(program, info.max_chunk_size)
] ++ [ExSPIKE.Messages.start_program(0)]
|> Enum.map(&ExSPIKE.encode/1)

Send each frame and wait for the hub's response before sending the next.

Livebook

Run in Livebook

notebooks/spike_prime.livemd connects to a hub over Web Bluetooth with KinoWebBluetooth, asks it for its info, shows its console output and sensor readings, and uploads and runs a program.

Modules

Module Purpose
ExSPIKE Encode/decode messages to/from frames
ExSPIKE.Messages Functions that build the messages a client sends
ExSPIKE.Message Message structs to/from raw bytes
ExSPIKE.Device Device messages inside a DeviceNotification
ExSPIKE.Frame Framing: COBS, XOR, delimiters, stream splitting
ExSPIKE.CRC CRC32 for file transfers
ExSPIKE.Enums Protocol enumerations as atoms

Development

mix test # BDD style tests and doctests
mix spec # tests, plus writes spec/STATUS.md
mix docs # generate documentation

Spec

The requirements are in spec/ex_spike.spec.md, each with an ID such as ARCH-4. Tests declare the requirements they verify with a tag:

@tag spec: "ARCH-4"
test "when the application starts, then it starts no processes" do

Requirements that tests can't fully cover are reviewed by hand and recorded in spec/reviews.exs.

mix spec runs the tests and writes spec/STATUS.md, which lists each requirement as tested, reviewed, failing or open. Commit it together with spec and code changes. CI fails when it is out of date.

Releasing

  1. Bump @version in mix.exs and merge to main.
  2. In GitHub, run Actions → Publish to Hex on main with that version. Tick Dry run first to check the package without publishing.

The workflow runs all checks, publishes the package and docs to Hex, and tags the release vX.Y.Z. It needs a HEX_API_KEY secret in the hex environment (Settings → Environments), where you can also add required reviewers.

License

MIT, see LICENSE.