Betaflight setup wizard

feat: basic msp encode/decode

brwr.dev b25ca8ca

+487
+23
.github/workflows/test.yml
··· 1 + name: test 2 + 3 + on: 4 + push: 5 + branches: 6 + - master 7 + - main 8 + pull_request: 9 + 10 + jobs: 11 + test: 12 + runs-on: ubuntu-latest 13 + steps: 14 + - uses: actions/checkout@v4 15 + - uses: erlef/setup-beam@v1 16 + with: 17 + otp-version: "28" 18 + gleam-version: "1.13.0" 19 + rebar3-version: "3" 20 + # elixir-version: "1" 21 + - run: gleam deps download 22 + - run: gleam test 23 + - run: gleam format --check src test
+8
.gitignore
··· 1 + *.beam 2 + *.ez 3 + /build 4 + erl_crash.dump 5 + 6 + #Added automatically by Lustre Dev Tools 7 + /.lustre 8 + /dist
+5
.mise.toml
··· 1 + [tools] 2 + bun = "latest" 3 + gleam = "latest" 4 + erlang = "latest" 5 + rebar = "latest"
+24
README.md
··· 1 + # betaflight_wizard 2 + 3 + [![Package Version](https://img.shields.io/hexpm/v/betaflight_wizard)](https://hex.pm/packages/betaflight_wizard) 4 + [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/betaflight_wizard/) 5 + 6 + ```sh 7 + gleam add betaflight_wizard@1 8 + ``` 9 + ```gleam 10 + import betaflight_wizard 11 + 12 + pub fn main() -> Nil { 13 + // TODO: An example of the project in use 14 + } 15 + ``` 16 + 17 + Further documentation can be found at <https://hexdocs.pm/betaflight_wizard>. 18 + 19 + ## Development 20 + 21 + ```sh 22 + gleam run # Run the project 23 + gleam test # Run the tests 24 + ```
+15
gleam.toml
··· 1 + name = "betaflight_wizard" 2 + version = "1.0.0" 3 + 4 + target = "javascript" 5 + 6 + [dependencies] 7 + gleam_stdlib = ">= 0.44.0 and < 2.0.0" 8 + lustre = ">= 5.4.0 and < 6.0.0" 9 + 10 + [dev-dependencies] 11 + gleeunit = ">= 1.0.0 and < 2.0.0" 12 + lustre_dev_tools = ">= 2.3.1 and < 3.0.0" 13 + 14 + [tools.lustre] 15 + bin.bun = "system"
+49
manifest.toml
··· 1 + # This file was generated by Gleam 2 + # You typically do not need to edit this file 3 + 4 + packages = [ 5 + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, 6 + { name = "booklet", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "booklet", source = "hex", outer_checksum = "08E0FDB78DC4D8A5D3C80295B021505C7D2A2E7B6C6D5EAB7286C36F4A53C851" }, 7 + { name = "directories", version = "1.2.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "D13090CFCDF6759B87217E8DDD73A75903A700148A82C1D33799F333E249BF9E" }, 8 + { name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" }, 9 + { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" }, 10 + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, 11 + { name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" }, 12 + { name = "gleam_community_colour", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "E34DD2C896AC3792151EDA939DA435FF3B69922F33415ED3C4406C932FBE9634" }, 13 + { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" }, 14 + { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, 15 + { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, 16 + { name = "gleam_httpc", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C545172618D07811494E97AAA4A0FB34DA6F6D0061FDC8041C2F8E3BE2B2E48F" }, 17 + { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, 18 + { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" }, 19 + { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, 20 + { name = "gleam_stdlib", version = "0.67.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6CE3E4189A8B8EC2F73AB61A2FBDE49F159D6C9C61C49E3B3082E439F260D3D0" }, 21 + { name = "gleam_time", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "0DF3834D20193F0A38D0EB21F0A78D48F2EC276C285969131B86DF8D4EF9E762" }, 22 + { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, 23 + { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, 24 + { name = "glint", version = "1.2.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "2214C7CEFDE457CEE62140C3D4899B964E05236DA74E4243DFADF4AF29C382BB" }, 25 + { name = "glisten", version = "8.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "534BB27C71FB9E506345A767C0D76B17A9E9199934340C975DC003C710E3692D" }, 26 + { name = "gramps", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "8B7195978FBFD30B43DF791A8A272041B81E45D245314D7A41FC57237AA882A0" }, 27 + { name = "group_registry", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "group_registry", source = "hex", outer_checksum = "BC798A53D6F2406DB94E27CB45C57052CB56B32ACF7CC16EA20F6BAEC7E36B90" }, 28 + { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" }, 29 + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, 30 + { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, 31 + { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, 32 + { name = "lustre", version = "5.4.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "40E097BABCE65FB7C460C073078611F7F5802EB07E1A9BFB5C229F71B60F8E50" }, 33 + { name = "lustre_dev_tools", version = "2.3.1", build_tools = ["gleam"], requirements = ["argv", "booklet", "filepath", "gleam_community_ansi", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_regexp", "gleam_stdlib", "glint", "group_registry", "justin", "lustre", "mist", "polly", "simplifile", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "2C8C646FF45087C31C2DE8088C2F6DB26E8CEE52B3A883F47F6B2C4F5A16C9C6" }, 34 + { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, 35 + { name = "mist", version = "5.0.3", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "7C4BE717A81305323C47C8A591E6B9BA4AC7F56354BF70B4D3DF08CC01192668" }, 36 + { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, 37 + { name = "polly", version = "2.1.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib", "simplifile"], otp_app = "polly", source = "hex", outer_checksum = "1BA4D0ACE9BCF52AEA6AD9DE020FD8220CCA399A379E50A1775FC5C1204FCF56" }, 38 + { name = "simplifile", version = "2.3.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "957E0E5B75927659F1D2A1B7B75D7B9BA96FAA8D0C53EA71C4AD9CD0C6B848F6" }, 39 + { name = "snag", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "274F41D6C3ECF99F7686FDCE54183333E41D2C1CA5A3A673F9A8B2C7A4401077" }, 40 + { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, 41 + { name = "tom", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "74D0C5A3761F7A7D06994755D4D5AD854122EF8E9F9F76A3E7547606D8C77091" }, 42 + { name = "wisp", version = "2.1.0", build_tools = ["gleam"], requirements = ["directories", "exception", "filepath", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "houdini", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "362BDDD11BF48EB38CDE51A73BC7D1B89581B395CA998E3F23F11EC026151C54" }, 43 + ] 44 + 45 + [requirements] 46 + gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 47 + gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 48 + lustre = { version = ">= 5.4.0 and < 6.0.0" } 49 + lustre_dev_tools = { version = ">= 2.3.1 and < 3.0.0" }
+36
src/betaflight_wizard.gleam
··· 1 + import gleam/io 2 + import lustre/element/html 3 + 4 + import lustre 5 + import lustre/element.{type Element} 6 + 7 + pub fn main() -> Nil { 8 + let app = lustre.simple(init, update, view) 9 + let assert Ok(_) = lustre.start(app, "#app", Nil) 10 + 11 + Nil 12 + } 13 + 14 + type Model = 15 + Int 16 + 17 + fn init(_args) -> Model { 18 + 0 19 + } 20 + 21 + type Msg { 22 + UserClicked 23 + } 24 + 25 + fn update(model: Model, msg: Msg) -> Model { 26 + case msg { 27 + UserClicked -> model + 1 28 + } 29 + } 30 + 31 + fn view(model: Model) -> Element(msg) { 32 + html.div([], [ 33 + html.h1([], [html.text("Waiting for FC...")]), 34 + html.button([], [html.text("Select your port")]), 35 + ]) 36 + }
+218
src/betaflight_wizard/msp/msp.gleam
··· 1 + import gleam/bit_array 2 + import gleam/bool 3 + import gleam/int 4 + import gleam/io 5 + import gleam/string 6 + 7 + const baud_rates = [ 8 + "AUTO", 9 + "9600", 10 + "19200", 11 + "38400", 12 + "57600", 13 + "115200", 14 + "230400", 15 + "250000", 16 + "400000", 17 + "460800", 18 + "500000", 19 + "921600", 20 + "1000000", 21 + "1500000", 22 + "2000000", 23 + "2470000", 24 + ] 25 + 26 + pub type PacketDirection { 27 + ToFc 28 + FromFc 29 + ErrorReport 30 + } 31 + 32 + pub type Packet { 33 + Packet 34 + } 35 + 36 + pub type ParseError { 37 + Incomplete 38 + Crc(expected: Int, actual: Int) 39 + } 40 + 41 + @internal 42 + pub fn calculate_crc_v1( 43 + size size: Int, 44 + cmd cmd: Int, 45 + data data: BitArray, 46 + crc crc: Int, 47 + ) -> Int { 48 + case data { 49 + <<b:size(8), rest:bytes>> -> 50 + calculate_crc_v1(size, cmd, rest, int.bitwise_exclusive_or(crc, b)) 51 + <<b:size(8)>> -> 52 + calculate_crc_v1(size, cmd, <<>>, int.bitwise_exclusive_or(crc, b)) 53 + _ -> 54 + size 55 + |> int.bitwise_exclusive_or(cmd) 56 + |> int.bitwise_exclusive_or(crc) 57 + } 58 + } 59 + 60 + @internal 61 + pub fn calculate_crc_v2( 62 + size size: Int, 63 + flag flag: Int, 64 + cmd cmd: Int, 65 + data data: BitArray, 66 + ) -> Int { 67 + 0 68 + |> crc_dvb_s2(flag) 69 + |> crc_dvb_s2(int.bitwise_and(cmd, 0xff)) 70 + |> crc_dvb_s2(cmd |> int.bitwise_and(0xff00) |> int.bitwise_shift_right(8)) 71 + |> crc_dvb_s2(int.bitwise_and(size, 0xff)) 72 + |> crc_dvb_s2(size |> int.bitwise_and(0xff00) |> int.bitwise_shift_right(8)) 73 + |> calculate_crc_v2_loop(data) 74 + } 75 + 76 + fn calculate_crc_v2_loop(crc: Int, data: BitArray) -> Int { 77 + case data { 78 + <<>> -> crc 79 + <<b:size(8)>> -> crc_dvb_s2(crc, b) 80 + <<b:size(8), rest:bytes>> -> 81 + crc_dvb_s2(crc, b) |> calculate_crc_v2_loop(rest) 82 + what -> 83 + panic as { 84 + "OOPS got invalid state while calculating crcv2 " 85 + <> string.inspect(what) 86 + } 87 + } 88 + } 89 + 90 + // what? 91 + fn crc_dvb_s2(crc: Int, byte: Int) -> Int { 92 + let crc = int.bitwise_exclusive_or(crc, byte) 93 + crc_dvb_s2_loop(crc, 0) 94 + } 95 + 96 + fn crc_dvb_s2_loop(crc: Int, i: Int) -> Int { 97 + case i { 98 + 8 -> crc 99 + i -> { 100 + case int.bitwise_and(crc, 0x80) == 0 { 101 + True -> crc |> int.bitwise_shift_left(1) |> int.bitwise_and(0xff) 102 + False -> 103 + crc 104 + |> int.bitwise_shift_left(1) 105 + |> int.bitwise_and(0xff) 106 + |> int.bitwise_exclusive_or(0xd5) 107 + } 108 + } 109 + } 110 + } 111 + 112 + fn parse_direction(dir: Int) -> Result(PacketDirection, Nil) { 113 + case dir { 114 + // < 115 + 0x3c -> Ok(ToFc) 116 + // > 117 + 0x3e -> Ok(FromFc) 118 + // ! 119 + 0x21 -> Ok(ErrorReport) 120 + _ -> Error(Nil) 121 + } 122 + } 123 + 124 + /// returns #(Result(Packet, ErrorReason), DataRest) 125 + fn parse_packet(packet: BitArray) -> #(Result(Packet, ParseError), BitArray) { 126 + case packet { 127 + // V1 Packet ($M header) 128 + <<"$M":utf8, dir:size(8), size:size(8), cmd:int-size(8), rest:bits>> -> 129 + case rest { 130 + <<data:bits-size(size), crc:size(8), rest:bits>> -> { 131 + let actual_crc = 132 + calculate_crc_v1(cmd: cmd, size: size, data: data, crc: 0) 133 + use <- bool.guard(when: crc != actual_crc, return: #( 134 + Error(Crc(expected: crc, actual: actual_crc)), 135 + rest, 136 + )) 137 + let assert Ok(dir) = parse_direction(dir) 138 + io.println( 139 + "got command " 140 + <> int.to_string(cmd) 141 + <> " with direction " 142 + <> string.inspect(dir) 143 + <> " with data " 144 + <> string.inspect(data), 145 + ) 146 + todo 147 + } 148 + _ -> #(Error(Incomplete), packet) 149 + } 150 + // V2 Packet ($X header) 151 + << 152 + "$X":utf8, 153 + dir:size(8), 154 + flag:size(8), 155 + cmd:little-size(16), 156 + size:little-size(16), 157 + rest:bits, 158 + >> -> 159 + case rest { 160 + <<data:bits-size(size), crc:size(8), rest:bits>> -> { 161 + let actual_crc = calculate_crc_v2(cmd:, size:, data:, flag:) 162 + use <- bool.guard(when: crc != actual_crc, return: #( 163 + Error(Crc(expected: crc, actual: actual_crc)), 164 + rest, 165 + )) 166 + let assert Ok(dir) = parse_direction(dir) 167 + io.println( 168 + "got v2 command " 169 + <> int.to_string(cmd) 170 + <> " with direction " 171 + <> string.inspect(dir) 172 + <> " with data " 173 + <> string.inspect(data), 174 + ) 175 + todo 176 + } 177 + _ -> #(Error(Incomplete), packet) 178 + } 179 + <<b:size(8), rest:bits>> -> { 180 + io.println_error("invalid packet, discarding byte " <> int.to_string(b)) 181 + parse_packet(rest) 182 + } 183 + _ -> #(Error(Incomplete), packet) 184 + } 185 + } 186 + 187 + @internal 188 + pub fn encode_v1_raw(command: Int, data: BitArray) -> BitArray { 189 + // preamble, direction, data size, command, data, crc 190 + let size = bit_array.byte_size(data) 191 + << 192 + "$M", 193 + "<", 194 + size, 195 + command, 196 + data:bits, 197 + calculate_crc_v1(command, size, data, 0), 198 + >> 199 + } 200 + 201 + @internal 202 + pub fn encode_v2_raw( 203 + command command: Int, 204 + flag flag: Int, 205 + data data: BitArray, 206 + ) -> BitArray { 207 + // preamble, direction, flag, cmd, size, data, crc 208 + let size = bit_array.byte_size(data) 209 + << 210 + "$X", 211 + "<", 212 + flag, 213 + command:little-size(16), 214 + size:little-size(16), 215 + data:bits, 216 + calculate_crc_v2(size:, data:, flag:, cmd: command), 217 + >> 218 + }
+5
test/betaflight_wizard_test.gleam
··· 1 + import gleeunit 2 + 3 + pub fn main() -> Nil { 4 + gleeunit.main() 5 + }
+104
test/msp_test.gleam
··· 1 + import betaflight_wizard/msp/msp 2 + import gleam/bit_array 3 + import gleam/int 4 + 5 + pub fn msp_encode_v1_empty_test() { 6 + do_msp_encode_v1_empty(0) 7 + } 8 + 9 + pub fn encode_random_v1_message_test() { 10 + let op_code = int.random(256) 11 + let input_data_length = 100 + int.random(100) 12 + let input_data = fill_input_data(input_data_length, 0, <<>>) 13 + 14 + let encoded = msp.encode_v1_raw(op_code, input_data) 15 + let encoded_length = bit_array.byte_size(encoded) 16 + 17 + assert bit_array.slice(encoded, 0, 3) == Ok(<<"$M":utf8, "<":utf8>>) 18 + assert bit_array.slice(encoded, 3, 1) == Ok(<<input_data_length:size(8)>>) 19 + assert bit_array.slice(encoded, 4, 1) == Ok(<<op_code:size(8)>>) 20 + assert bit_array.slice(encoded, 5, input_data_length) == Ok(input_data) 21 + assert bit_array.slice(encoded, encoded_length - 1, 1) 22 + == Ok(<< 23 + msp.calculate_crc_v1(input_data_length, op_code, input_data, 0):size(8), 24 + >>) 25 + } 26 + 27 + pub fn msp_encode_v2_empty_test() { 28 + do_msp_encode_v2_empty(0) 29 + } 30 + 31 + pub fn encode_random_v2_message_test() { 32 + let op_code = int.random(65_536) 33 + let flag = int.random(256) 34 + let input_data_length = int.random(65_536) 35 + let input_data = fill_input_data(input_data_length, 0, <<>>) 36 + 37 + let encoded = msp.encode_v2_raw(command: op_code, flag:, data: input_data) 38 + let encoded_length = bit_array.byte_size(encoded) 39 + 40 + // preamble, direction, flag, cmd low, cmd high, size, data, crc 41 + // preamble, direction 42 + assert bit_array.slice(encoded, 0, 3) == Ok(<<"$X", "<">>) 43 + // flag 44 + assert bit_array.slice(encoded, 3, 1) == Ok(<<flag>>) 45 + // cmd 46 + assert bit_array.slice(encoded, 4, 2) == Ok(<<op_code:little-size(16)>>) 47 + // size 48 + assert bit_array.slice(encoded, 6, 2) 49 + == Ok(<<input_data_length:little-size(16)>>) 50 + // data 51 + assert bit_array.slice(encoded, 8, input_data_length) == Ok(input_data) 52 + // crc 53 + assert bit_array.slice(encoded, encoded_length - 1, 1) 54 + == Ok(<< 55 + msp.calculate_crc_v2( 56 + size: input_data_length, 57 + flag:, 58 + cmd: op_code, 59 + data: input_data, 60 + ), 61 + >>) 62 + } 63 + 64 + fn do_msp_encode_v1_empty(cmd: Int) { 65 + case cmd { 66 + 256 -> Nil 67 + _ -> { 68 + let encoded = msp.encode_v1_raw(cmd, <<>>) 69 + // preamble, direction, data size, command, data, crc 70 + assert encoded == <<"$M", "<", 0, cmd, cmd>> 71 + do_msp_encode_v1_empty(cmd + 1) 72 + } 73 + } 74 + } 75 + 76 + fn fill_input_data(input_data_length: Int, n: Int, data: BitArray) -> BitArray { 77 + case n { 78 + n if n >= input_data_length -> data 79 + n -> 80 + fill_input_data(input_data_length, n + 1, <<data:bits, int.random(256)>>) 81 + } 82 + } 83 + 84 + fn do_msp_encode_v2_empty(cmd: Int) { 85 + case cmd { 86 + // MSPv2 commands are 16bit rather than 8bit 87 + 65_536 -> Nil 88 + _ -> { 89 + let encoded = msp.encode_v2_raw(command: cmd, flag: 0, data: <<>>) 90 + // preamble, direction, flag, cmd low, cmd high, size, data, crc 91 + assert encoded 92 + == << 93 + "$X", 94 + "<", 95 + 0, 96 + cmd:little-size(16), 97 + 0:size(16), 98 + // skip data 99 + msp.calculate_crc_v2(size: 0, cmd: cmd, flag: 0, data: <<>>), 100 + >> 101 + do_msp_encode_v2_empty(cmd + 1) 102 + } 103 + } 104 + }