AX.25 Amateur Radio Link-Layer Protocol
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

irmin: consistent naming — Backend.S has type t, phantom tag is B.t

+130 -93
+17 -4
lib/ax25.ml
··· 345 345 346 346 (* {1 Encoding/Decoding} *) 347 347 348 - let encode_address_to_bytes addr = 348 + (** Determine whether a control field is a command (vs response). AX.25 2.2 349 + Section 6.1.2: UI, SABM, DISC are always commands; UA, DM, FRMR are 350 + responses; I/S frames default to command. *) 351 + let is_command = function 352 + | UI | SABM | DISC -> true 353 + | UA | DM | FRMR -> false 354 + | I _ | RR _ | RNR _ | REJ _ | Unknown _ -> true 355 + 356 + let encode_address_to_bytes ~control addr = 349 357 let all = 350 358 let n_digis = List.length addr.digipeaters in 351 359 let no_digis = n_digis = 0 in 360 + (* AX.25 2.2 Section 6.1.2: command = dst C=1, src C=0; 361 + response = dst C=0, src C=1. Digipeater H bit = has_been_repeated. *) 362 + let cmd = is_command control in 352 363 let dst = 353 364 callsign_wire_of_callsign addr.destination ~is_last:false 354 - ~has_been_repeated:false 365 + ~has_been_repeated:cmd 355 366 in 356 367 let src = 357 368 callsign_wire_of_callsign addr.source ~is_last:no_digis 358 - ~has_been_repeated:false 369 + ~has_been_repeated:(not cmd) 359 370 in 360 371 let digis = 361 372 List.mapi ··· 398 409 let write_to_buf frame = 399 410 let buf = Buffer.create 512 in 400 411 (* Address field *) 401 - let addr_bytes = encode_address_to_bytes frame.address in 412 + let addr_bytes = 413 + encode_address_to_bytes ~control:frame.control frame.address 414 + in 402 415 Buffer.add_bytes buf addr_bytes; 403 416 (* Control *) 404 417 let ctrl_buf = Wire.encode_to_bytes control_typ frame.control in
+1 -1
test/interop/pyax25/dune
··· 1 1 (test 2 2 (name test) 3 - (libraries ax25 alcotest) 3 + (libraries ax25 csvt alcotest) 4 4 (deps 5 5 (source_tree traces) 6 6 (source_tree scripts)))
+5
test/interop/pyax25/scripts/generate.py
··· 44 44 45 45 # Basic UI frame, no digipeaters 46 46 f = AX25UnnumberedInformationFrame( 47 + cr=True, 47 48 destination=addr("CQ"), 48 49 source=addr("N0CALL", 1), 49 50 pid=0xF0, ··· 56 57 57 58 # UI frame with digipeater 58 59 f = AX25UnnumberedInformationFrame( 60 + cr=True, 59 61 destination=addr("W1AW"), 60 62 source=addr("N0CALL"), 61 63 repeaters=[AX25Address.decode("RELAY").normalised], ··· 69 71 70 72 # NJ7P > N7LEM — spec example 71 73 f = AX25UnnumberedInformationFrame( 74 + cr=True, 72 75 destination=addr("NJ7P"), 73 76 source=addr("N7LEM"), 74 77 pid=0xF0, ··· 79 82 80 83 # Source SSID=0, no digis — the regression case 81 84 f = AX25UnnumberedInformationFrame( 85 + cr=True, 82 86 destination=addr("BEACON"), 83 87 source=addr("TEST"), 84 88 pid=0xF0, ··· 89 93 90 94 # IP over AX.25 91 95 f = AX25UnnumberedInformationFrame( 96 + cr=True, 92 97 destination=addr("CQ"), 93 98 source=addr("N0CALL"), 94 99 pid=0xCC,
+107 -88
test/interop/pyax25/test.ml
··· 5 5 6 6 let trace path = Filename.concat "traces" path 7 7 8 - let strip_cr s = 9 - if String.length s > 0 && s.[String.length s - 1] = '\r' then 10 - String.sub s 0 (String.length s - 1) 11 - else s 8 + (* {1 Trace Row Types} *) 9 + 10 + type ui_frame_row = { 11 + name : string; 12 + source : string; 13 + destination : string; 14 + digipeaters : string; 15 + pid_hex : string; 16 + info_hex : string; 17 + frame_hex : string; 18 + } 12 19 13 - let read_csv path = 14 - let ic = open_in (trace path) in 15 - let _header = input_line ic in 16 - let rows = ref [] in 17 - (try 18 - while true do 19 - let line = strip_cr (input_line ic) in 20 - if String.length line > 0 && line.[0] <> '#' then 21 - rows := String.split_on_char ',' line :: !rows 22 - done 23 - with End_of_file -> ()); 24 - close_in ic; 25 - List.rev !rows 20 + let ui_frame_codec = 21 + Csvt.( 22 + Row.( 23 + obj (fun name source destination digipeaters pid_hex info_hex frame_hex -> 24 + { 25 + name; 26 + source; 27 + destination; 28 + digipeaters; 29 + pid_hex; 30 + info_hex; 31 + frame_hex; 32 + }) 33 + |> col "name" string ~enc:(fun r -> r.name) 34 + |> col "source" string ~enc:(fun r -> r.source) 35 + |> col "destination" string ~enc:(fun r -> r.destination) 36 + |> col "digipeaters" string ~enc:(fun r -> r.digipeaters) 37 + |> col "pid_hex" string ~enc:(fun r -> r.pid_hex) 38 + |> col "info_hex" string ~enc:(fun r -> r.info_hex) 39 + |> col "frame_hex" string ~enc:(fun r -> r.frame_hex) 40 + |> finish)) 41 + 42 + type ext_bit_row = { 43 + name : string; 44 + frame_hex : string; 45 + n_callsigns : int; 46 + last_index : int; 47 + } 48 + 49 + let ext_bit_codec = 50 + Csvt.( 51 + Row.( 52 + obj (fun name frame_hex n_callsigns last_index -> 53 + { name; frame_hex; n_callsigns; last_index }) 54 + |> col "name" string ~enc:(fun r -> r.name) 55 + |> col "frame_hex" string ~enc:(fun r -> r.frame_hex) 56 + |> col "n_callsigns" int ~enc:(fun r -> r.n_callsigns) 57 + |> col "last_index" int ~enc:(fun r -> r.last_index) 58 + |> finish)) 59 + 60 + (* {1 Helpers} *) 26 61 27 62 let hex_to_bytes hex = 28 63 let len = String.length hex / 2 in ··· 51 86 (* {1 UI Frame Interop Tests} *) 52 87 53 88 let test_ui_frame_encoding () = 54 - let rows = read_csv "ui_frames.csv" in 89 + let rows = 90 + match Csvt.decode_file ui_frame_codec (trace "ui_frames.csv") with 91 + | Ok rows -> rows 92 + | Error e -> Alcotest.failf "CSV decode: %a" Csvt.pp_error e 93 + in 55 94 List.iter 56 - (fun row -> 57 - match row with 58 - | [ name; src_str; dst_str; _digis; _pid_hex; _info_hex; frame_hex ] -> ( 59 - (* Build the frame with our implementation *) 60 - let src = Ax25.callsign_of_string src_str in 61 - let dst = Ax25.callsign_of_string dst_str in 62 - match (src, dst) with 63 - | Some _src, Some _dst -> ( 64 - let ref_bytes = hex_to_bytes frame_hex in 65 - (* Our decoder must successfully parse frames from aioax25 *) 66 - match Ax25.decode ref_bytes with 67 - | Ok frame -> ( 68 - (* Verify decoded addresses match the trace metadata *) 69 - Alcotest.(check string) 70 - (name ^ " src") src_str 71 - (Ax25.string_of_callsign frame.address.source); 72 - Alcotest.(check string) 73 - (name ^ " dst") dst_str 74 - (Ax25.string_of_callsign frame.address.destination); 75 - (* Re-encode and verify the frame is parseable *) 76 - let our_bytes = Ax25.encode frame in 77 - match Ax25.decode our_bytes with 78 - | Ok frame2 -> 79 - Alcotest.(check string) 80 - (name ^ " re-decode src") 81 - (Ax25.string_of_callsign frame.address.source) 82 - (Ax25.string_of_callsign frame2.address.source) 83 - | Error e -> 84 - Alcotest.failf "%s: re-decode failed: %a" name 85 - Ax25.pp_error e) 86 - | Error e -> 87 - Alcotest.failf "%s: failed to decode reference frame: %a" name 88 - Ax25.pp_error e) 89 - | _ -> Alcotest.failf "%s: bad callsign in trace" name) 90 - | _ -> Alcotest.failf "bad CSV row: %d fields" (List.length row)) 95 + (fun (r : ui_frame_row) -> 96 + let ref_bytes = hex_to_bytes r.frame_hex in 97 + (* Our decoder must successfully parse frames from aioax25 *) 98 + match Ax25.decode ref_bytes with 99 + | Ok frame -> 100 + Alcotest.(check string) 101 + (r.name ^ " src") r.source 102 + (Ax25.string_of_callsign frame.address.source); 103 + Alcotest.(check string) 104 + (r.name ^ " dst") r.destination 105 + (Ax25.string_of_callsign frame.address.destination); 106 + (* Re-encode and compare bytes exactly against aioax25 *) 107 + let our_bytes = Ax25.encode frame in 108 + Alcotest.(check string) 109 + (r.name ^ " exact bytes") r.frame_hex (bytes_to_hex our_bytes) 110 + | Error e -> 111 + Alcotest.failf "%s: failed to decode reference frame: %a" r.name 112 + Ax25.pp_error e) 91 113 rows 92 114 93 115 (* {1 Extension Bit Interop Tests} *) 94 116 95 117 let test_extension_bits () = 96 - let rows = read_csv "extension_bits.csv" in 118 + let rows = 119 + match Csvt.decode_file ext_bit_codec (trace "extension_bits.csv") with 120 + | Ok rows -> rows 121 + | Error e -> Alcotest.failf "CSV decode: %a" Csvt.pp_error e 122 + in 97 123 List.iter 98 - (fun row -> 99 - match row with 100 - | [ name; frame_hex; n_str; last_str ] -> ( 101 - let n_callsigns = int_of_string n_str in 102 - let last_index = int_of_string last_str in 103 - let frame_bytes = hex_to_bytes frame_hex in 104 - (* Check extension bits in the raw reference bytes *) 105 - for i = 0 to n_callsigns - 1 do 124 + (fun (r : ext_bit_row) -> 125 + let frame_bytes = hex_to_bytes r.frame_hex in 126 + (* Check extension bits in the raw reference bytes *) 127 + for i = 0 to r.n_callsigns - 1 do 128 + let ssid_off = (i * 7) + 6 in 129 + let ext = Bytes.get_uint8 frame_bytes ssid_off land 0x01 in 130 + if i = r.last_index then 131 + Alcotest.(check int) 132 + (Printf.sprintf "%s: callsign %d ext=1 (last)" r.name i) 133 + 1 ext 134 + else 135 + Alcotest.(check int) 136 + (Printf.sprintf "%s: callsign %d ext=0" r.name i) 137 + 0 ext 138 + done; 139 + (* Decode with our implementation and re-encode *) 140 + match Ax25.decode frame_bytes with 141 + | Ok frame -> 142 + let our_bytes = Ax25.encode frame in 143 + for i = 0 to r.n_callsigns - 1 do 106 144 let ssid_off = (i * 7) + 6 in 107 - let ext = Bytes.get_uint8 frame_bytes ssid_off land 0x01 in 108 - if i = last_index then 109 - Alcotest.(check int) 110 - (Printf.sprintf "%s: callsign %d ext=1 (last)" name i) 111 - 1 ext 112 - else 113 - Alcotest.(check int) 114 - (Printf.sprintf "%s: callsign %d ext=0" name i) 115 - 0 ext 116 - done; 117 - (* Decode with our implementation and re-encode *) 118 - match Ax25.decode frame_bytes with 119 - | Ok frame -> 120 - let our_bytes = Ax25.encode frame in 121 - (* Our encoded bytes include FCS; strip it for address comparison *) 122 - for i = 0 to n_callsigns - 1 do 123 - let ssid_off = (i * 7) + 6 in 124 - let our_ext = Bytes.get_uint8 our_bytes ssid_off land 0x01 in 125 - let ref_ext = Bytes.get_uint8 frame_bytes ssid_off land 0x01 in 126 - Alcotest.(check int) 127 - (Printf.sprintf "%s: our ext[%d] matches ref" name i) 128 - ref_ext our_ext 129 - done 130 - | Error e -> 131 - Alcotest.failf "%s: decode failed: %a" name Ax25.pp_error e) 132 - | _ -> Alcotest.failf "bad CSV row") 145 + let our_ext = Bytes.get_uint8 our_bytes ssid_off land 0x01 in 146 + let ref_ext = Bytes.get_uint8 frame_bytes ssid_off land 0x01 in 147 + Alcotest.(check int) 148 + (Printf.sprintf "%s: our ext[%d] matches ref" r.name i) 149 + ref_ext our_ext 150 + done 151 + | Error e -> Alcotest.failf "%s: decode failed: %a" r.name Ax25.pp_error e) 133 152 rows 134 153 135 154 let () =