Jacob Erlbeck | da82df8 | 2013-12-13 13:18:20 +0100 | [diff] [blame] | 1 | #!/usr/bin/env escript |
| 2 | %% -*- erlang -*- |
| 3 | %%! -smp disable |
| 4 | -module(gen_rtp_header). |
| 5 | |
| 6 | % -mode(compile). |
| 7 | |
| 8 | -define(VERSION, "0.1"). |
| 9 | |
| 10 | -export([main/1]). |
| 11 | |
| 12 | -record(rtp_packet, |
| 13 | { |
| 14 | version = 2, |
| 15 | padding = 0, |
| 16 | marker = 0, |
| 17 | payload_type = 0, |
| 18 | seqno = 0, |
| 19 | timestamp = 0, |
| 20 | ssrc = 0, |
| 21 | csrcs = [], |
| 22 | extension = <<>>, |
| 23 | payload = <<>>, |
| 24 | realtime |
| 25 | }). |
| 26 | |
| 27 | |
| 28 | main(Args) -> |
| 29 | DefaultOpts = [{format, state}, |
| 30 | {ssrc, 16#11223344}, |
Jacob Erlbeck | 8c43ce6 | 2014-03-14 18:06:23 +0100 | [diff] [blame] | 31 | {rate, 8000}, |
Jacob Erlbeck | da82df8 | 2013-12-13 13:18:20 +0100 | [diff] [blame] | 32 | {pt, 98}], |
| 33 | {PosArgs, Opts} = getopts_checked(Args, DefaultOpts), |
| 34 | log(debug, fun (Dev) -> |
| 35 | io:format(Dev, "Initial options:~n", []), |
| 36 | dump_opts(Dev, Opts), |
| 37 | io:format(Dev, "~s: ~p~n", ["Args", PosArgs]) |
| 38 | end, [], Opts), |
| 39 | main(PosArgs, Opts). |
| 40 | |
| 41 | main([First | RemArgs], Opts) -> |
| 42 | try |
| 43 | F = list_to_integer(First), |
| 44 | Format = proplists:get_value(format, Opts, state), |
| 45 | PayloadData = proplists:get_value(payload, Opts, undef), |
| 46 | InFile = proplists:get_value(file, Opts, undef), |
| 47 | |
| 48 | Payload = case {PayloadData, InFile} of |
Jacob Erlbeck | 3408c9c | 2013-12-18 12:43:18 +0100 | [diff] [blame] | 49 | {undef, undef} -> |
| 50 | % use default value |
| 51 | #rtp_packet{}#rtp_packet.payload; |
Jacob Erlbeck | da82df8 | 2013-12-13 13:18:20 +0100 | [diff] [blame] | 52 | {P, undef} -> P; |
| 53 | {_, File} -> |
| 54 | log(info, "Loading file '~s'~n", [File], Opts), |
| 55 | {ok, InDev} = file:open(File, [read]), |
| 56 | DS = [ Pl#rtp_packet.payload || {_T, Pl} <- read_packets(InDev, Opts)], |
| 57 | file:close(InDev), |
| 58 | log(debug, "File '~s' closed, ~w packets read.~n", [File, length(DS)], Opts), |
| 59 | DS |
| 60 | end, |
| 61 | Dev = standard_io, |
| 62 | write_packet_pre(Dev, Format), |
| 63 | do_groups(Dev, Payload, F, RemArgs, Opts), |
| 64 | write_packet_post(Dev, Format), |
| 65 | 0 |
| 66 | catch |
| 67 | _:_ -> |
| 68 | log(debug, "~p~n", [hd(erlang:get_stacktrace())], Opts), |
| 69 | usage(), |
| 70 | halt(1) |
| 71 | end |
| 72 | ; |
| 73 | |
| 74 | main(_, _Opts) -> |
| 75 | usage(), |
| 76 | halt(1). |
| 77 | |
| 78 | %%% group (count + offset) handling %%% |
| 79 | |
| 80 | do_groups(_Dev, _Pl, _F, [], _Opts) -> |
| 81 | ok; |
| 82 | |
| 83 | do_groups(Dev, Pl, F, [L], Opts) -> |
| 84 | do_groups(Dev, Pl, F, [L, 0], Opts); |
| 85 | |
| 86 | do_groups(Dev, Pl, First, [L, O | Args], Opts) -> |
| 87 | Ssrc = proplists:get_value(ssrc, Opts, #rtp_packet.ssrc), |
| 88 | PT = proplists:get_value(pt, Opts, #rtp_packet.payload_type), |
| 89 | Len = list_to_num(L), |
| 90 | Offs = list_to_num(O), |
| 91 | log(info, "Starting group: Ssrc=~.16B, PT=~B, First=~B, Len=~B, Offs=~B~n", |
| 92 | [Ssrc, PT, First, Len, Offs], Opts), |
| 93 | Pkg = #rtp_packet{ssrc = Ssrc, payload_type = PT}, |
| 94 | Pl2 = write_packets(Dev, Pl, Pkg, First, Len, Offs, Opts), |
| 95 | {Args2, Opts2} = getopts_checked(Args, Opts), |
| 96 | log(debug, fun (Io) -> |
| 97 | io:format(Io, "Changed options:~n", []), |
| 98 | dump_opts(Io, Opts2 -- Opts) |
| 99 | end, [], Opts), |
| 100 | do_groups(Dev, Pl2, First+Len, Args2, Opts2). |
| 101 | |
| 102 | %%% error handling helpers %%% |
| 103 | |
| 104 | getopts_checked(Args, Opts) -> |
| 105 | try |
| 106 | getopts(Args, Opts) |
| 107 | catch |
| 108 | C:R -> |
| 109 | log(error, "~s~n", |
| 110 | [explain_error(C, R, erlang:get_stacktrace(), Opts)], Opts), |
| 111 | usage(), |
| 112 | halt(1) |
| 113 | end. |
| 114 | |
| 115 | explain_error(error, badarg, [{erlang,list_to_integer,[S,B]} | _ ], _Opts) -> |
| 116 | io_lib:format("Invalid number '~s' (base ~B)", [S, B]); |
| 117 | explain_error(error, badarg, [{erlang,list_to_integer,[S]} | _ ], _Opts) -> |
| 118 | io_lib:format("Invalid decimal number '~s'", [S]); |
| 119 | explain_error(C, R, [Hd | _ ], _Opts) -> |
| 120 | io_lib:format("~p, ~p:~p", [Hd, C, R]); |
| 121 | explain_error(_, _, [], _Opts) -> |
| 122 | "". |
| 123 | |
| 124 | %%% usage and options %%% |
| 125 | |
| 126 | myname() -> |
| 127 | filename:basename(escript:script_name()). |
| 128 | |
| 129 | usage(Text) -> |
| 130 | io:format(standard_error, "~s: ~s~n", [myname(), Text]), |
| 131 | usage(). |
| 132 | |
| 133 | usage() -> |
| 134 | io:format(standard_error, |
| 135 | "Usage: ~s [Options] Start Count1 Offs1 [[Options] Count2 Offs2 ...]~n", |
| 136 | [myname()]). |
| 137 | |
| 138 | show_version() -> |
| 139 | io:format(standard_io, |
| 140 | "~s ~s~n", [myname(), ?VERSION]). |
| 141 | |
| 142 | show_help() -> |
| 143 | io:format(standard_io, |
| 144 | "Usage: ~s [Options] Start Count1 Offs1 [[Options] Count2 Offs2 ...]~n~n" ++ |
| 145 | "Options:~n" ++ |
| 146 | " -h, --help this text~n" ++ |
| 147 | " --version show version info~n" ++ |
Jacob Erlbeck | 8c43ce6 | 2014-03-14 18:06:23 +0100 | [diff] [blame] | 148 | " -i, --file=FILE reads payload from file (state format by default)~n" ++ |
| 149 | " -f, --frame-size=N read payload as binary frames of size N instead~n" ++ |
Jacob Erlbeck | da82df8 | 2013-12-13 13:18:20 +0100 | [diff] [blame] | 150 | " -p, --payload=HEX set constant payload~n" ++ |
| 151 | " --verbose=N set verbosity~n" ++ |
| 152 | " -v increase verbosity~n" ++ |
| 153 | " --format=state use state format for output (default)~n" ++ |
| 154 | " -C, --format=c use simple C lines for output~n" ++ |
| 155 | " --format=carray use a C array for output~n" ++ |
| 156 | " -s, --ssrc=SSRC set the SSRC~n" ++ |
| 157 | " -t, --type=N set the payload type~n" ++ |
Jacob Erlbeck | 8c43ce6 | 2014-03-14 18:06:23 +0100 | [diff] [blame] | 158 | " -r, --rate=N set the RTP rate [8000]~n" ++ |
| 159 | " -D, --duration=N set the packet duration in RTP time units [160]~n" ++ |
Jacob Erlbeck | da82df8 | 2013-12-13 13:18:20 +0100 | [diff] [blame] | 160 | " -d, --delay=FLOAT add offset to playout timestamp~n" ++ |
| 161 | "~n" ++ |
| 162 | "Arguments:~n" ++ |
| 163 | " Start initial packet (sequence) number~n" ++ |
| 164 | " Count number of packets~n" ++ |
| 165 | " Offs timestamp offset (in RTP units)~n" ++ |
| 166 | "", [myname()]). |
| 167 | |
| 168 | getopts([ "--file=" ++ File | R], Opts) -> |
| 169 | getopts(R, [{file, File} | Opts]); |
| 170 | getopts([ "-i" ++ T | R], Opts) -> |
| 171 | getopts_alias_arg("--file", T, R, Opts); |
Jacob Erlbeck | 8c43ce6 | 2014-03-14 18:06:23 +0100 | [diff] [blame] | 172 | getopts([ "--frame-size=" ++ N | R], Opts) -> |
| 173 | Size = list_to_integer(N), |
| 174 | getopts(R, [{frame_size, Size}, {in_format, bin} | Opts]); |
| 175 | getopts([ "-f" ++ T | R], Opts) -> |
| 176 | getopts_alias_arg("--frame-size", T, R, Opts); |
| 177 | getopts([ "--duration=" ++ N | R], Opts) -> |
| 178 | Duration = list_to_integer(N), |
| 179 | getopts(R, [{duration, Duration} | Opts]); |
| 180 | getopts([ "-D" ++ T | R], Opts) -> |
| 181 | getopts_alias_arg("--duration", T, R, Opts); |
| 182 | getopts([ "--rate=" ++ N | R], Opts) -> |
| 183 | Rate = list_to_integer(N), |
| 184 | getopts(R, [{rate, Rate} | Opts]); |
| 185 | getopts([ "-r" ++ T | R], Opts) -> |
| 186 | getopts_alias_arg("--rate", T, R, Opts); |
Jacob Erlbeck | da82df8 | 2013-12-13 13:18:20 +0100 | [diff] [blame] | 187 | getopts([ "--version" | _], _Opts) -> |
| 188 | show_version(), |
| 189 | halt(0); |
| 190 | getopts([ "--help" | _], _Opts) -> |
| 191 | show_help(), |
| 192 | halt(0); |
| 193 | getopts([ "-h" ++ T | R], Opts) -> |
| 194 | getopts_alias_no_arg("--help", T, R, Opts); |
| 195 | getopts([ "--verbose=" ++ V | R], Opts) -> |
| 196 | Verbose = list_to_integer(V), |
| 197 | getopts(R, [{verbose, Verbose} | Opts]); |
| 198 | getopts([ "-v" ++ T | R], Opts) -> |
| 199 | Verbose = proplists:get_value(verbose, Opts, 0), |
| 200 | getopts_short_no_arg(T, R, [ {verbose, Verbose+1} | Opts]); |
| 201 | getopts([ "--format=state" | R], Opts) -> |
| 202 | getopts(R, [{format, state} | Opts]); |
| 203 | getopts([ "--format=c" | R], Opts) -> |
| 204 | getopts(R, [{format, c} | Opts]); |
| 205 | getopts([ "-C" ++ T | R], Opts) -> |
| 206 | getopts_alias_no_arg("--format=c", T, R, Opts); |
| 207 | getopts([ "--format=carray" | R], Opts) -> |
| 208 | getopts(R, [{format, carray} | Opts]); |
| 209 | getopts([ "--payload=" ++ Hex | R], Opts) -> |
| 210 | getopts(R, [{payload, hex_to_bin(Hex)} | Opts]); |
| 211 | getopts([ "--ssrc=" ++ Num | R], Opts) -> |
| 212 | getopts(R, [{ssrc, list_to_num(Num)} | Opts]); |
| 213 | getopts([ "-s" ++ T | R], Opts) -> |
| 214 | getopts_alias_arg("--ssrc", T, R, Opts); |
| 215 | getopts([ "--type=" ++ Num | R], Opts) -> |
| 216 | getopts(R, [{pt, list_to_num(Num)} | Opts]); |
| 217 | getopts([ "-t" ++ T | R], Opts) -> |
| 218 | getopts_alias_arg("--type", T, R, Opts); |
| 219 | getopts([ "--delay=" ++ Num | R], Opts) -> |
| 220 | getopts(R, [{delay, list_to_float(Num)} | Opts]); |
| 221 | getopts([ "-d" ++ T | R], Opts) -> |
| 222 | getopts_alias_arg("--delay", T, R, Opts); |
| 223 | |
| 224 | % parsing helpers |
| 225 | getopts([ "--" | R], Opts) -> |
| 226 | {R, normalize_opts(Opts)}; |
| 227 | getopts([ O = "--" ++ _ | _], _Opts) -> |
| 228 | usage("Invalid option: " ++ O), |
| 229 | halt(1); |
| 230 | getopts([ [ $-, C | _] | _], _Opts) when C < $0; C > $9 -> |
| 231 | usage("Invalid option: -" ++ [C]), |
| 232 | halt(1); |
| 233 | |
| 234 | getopts(R, Opts) -> |
| 235 | {R, normalize_opts(Opts)}. |
| 236 | |
| 237 | getopts_short_no_arg([], R, Opts) -> getopts(R, Opts); |
| 238 | getopts_short_no_arg(T, R, Opts) -> getopts([ "-" ++ T | R], Opts). |
| 239 | |
| 240 | getopts_alias_no_arg(A, [], R, Opts) -> getopts([A | R], Opts); |
| 241 | getopts_alias_no_arg(A, T, R, Opts) -> getopts([A, "-" ++ T | R], Opts). |
| 242 | |
| 243 | getopts_alias_arg(A, [], [T | R], Opts) -> getopts([A ++ "=" ++ T | R], Opts); |
| 244 | getopts_alias_arg(A, T, R, Opts) -> getopts([A ++ "=" ++ T | R], Opts). |
| 245 | |
| 246 | normalize_opts(Opts) -> |
| 247 | [ proplists:lookup(E, Opts) || E <- proplists:get_keys(Opts) ]. |
| 248 | |
| 249 | %%% conversions %%% |
| 250 | |
| 251 | bin_to_hex(Bin) -> [hd(integer_to_list(N,16)) || <<N:4>> <= Bin]. |
| 252 | hex_to_bin(Hex) -> << <<(list_to_integer([Nib],16)):4>> || Nib <- Hex>>. |
| 253 | |
| 254 | list_to_num("-" ++ Str) -> -list_to_num(Str); |
| 255 | list_to_num("0x" ++ Str) -> list_to_integer(Str, 16); |
| 256 | list_to_num("0b" ++ Str) -> list_to_integer(Str, 2); |
| 257 | list_to_num(Str = [ $0 | _ ]) -> list_to_integer(Str, 8); |
| 258 | list_to_num(Str) -> list_to_integer(Str, 10). |
| 259 | |
| 260 | %%% dumping data %%% |
| 261 | |
| 262 | dump_opts(Dev, Opts) -> |
| 263 | dump_opts2(Dev, Opts, proplists:get_keys(Opts)). |
| 264 | |
| 265 | dump_opts2(Dev, Opts, [OptName | R]) -> |
| 266 | io:format(Dev, " ~-10s: ~p~n", |
| 267 | [OptName, proplists:get_value(OptName, Opts)]), |
| 268 | dump_opts2(Dev, Opts, R); |
| 269 | dump_opts2(_Dev, _Opts, []) -> ok. |
| 270 | |
| 271 | %%% logging %%% |
| 272 | |
| 273 | log(L, Fmt, Args, Opts) when is_list(Opts) -> |
| 274 | log(L, Fmt, Args, proplists:get_value(verbose, Opts, 0), Opts). |
| 275 | |
| 276 | log(debug, Fmt, Args, V, Opts) when V > 2 -> log2("DEBUG", Fmt, Args, Opts); |
| 277 | log(info, Fmt, Args, V, Opts) when V > 1 -> log2("INFO", Fmt, Args, Opts); |
| 278 | log(notice, Fmt, Args, V, Opts) when V > 0 -> log2("NOTICE", Fmt, Args, Opts); |
| 279 | log(warn, Fmt, Args, _V, Opts) -> log2("WARNING", Fmt, Args, Opts); |
| 280 | log(error, Fmt, Args, _V, Opts) -> log2("ERROR", Fmt, Args, Opts); |
| 281 | |
| 282 | log(Lvl, Fmt, Args, V, Opts) when V >= Lvl -> log2("", Fmt, Args, Opts); |
| 283 | |
| 284 | log(_, _, _, _i, _) -> ok. |
| 285 | |
| 286 | log2(Type, Fmt, Args, _Opts) when is_list(Fmt) -> |
| 287 | io:format(standard_error, "~s: " ++ Fmt, [Type | Args]); |
| 288 | log2("", Fmt, Args, _Opts) when is_list(Fmt) -> |
| 289 | io:format(standard_error, Fmt, Args); |
| 290 | log2(_Type, Fun, _Args, _Opts) when is_function(Fun, 1) -> |
| 291 | Fun(standard_error). |
| 292 | |
| 293 | %%% RTP packets %%% |
| 294 | |
| 295 | make_rtp_packet(P = #rtp_packet{version = 2}) -> |
| 296 | << (P#rtp_packet.version):2, |
| 297 | 0:1, % P |
| 298 | 0:1, % X |
| 299 | 0:4, % CC |
| 300 | (P#rtp_packet.marker):1, |
| 301 | (P#rtp_packet.payload_type):7, |
| 302 | (P#rtp_packet.seqno):16, |
| 303 | (P#rtp_packet.timestamp):32, |
| 304 | (P#rtp_packet.ssrc):32, |
| 305 | (P#rtp_packet.payload)/bytes |
| 306 | >>. |
| 307 | |
| 308 | parse_rtp_packet( |
| 309 | << 2:2, % Version 2 |
| 310 | 0:1, % P (not supported yet) |
| 311 | 0:1, % X (not supported yet) |
| 312 | 0:4, % CC (not supported yet) |
| 313 | M:1, |
| 314 | PT:7, |
| 315 | SeqNo: 16, |
| 316 | TS:32, |
| 317 | Ssrc:32, |
| 318 | Payload/bytes >>) -> |
| 319 | #rtp_packet{ |
| 320 | version = 0, |
| 321 | marker = M, |
| 322 | payload_type = PT, |
| 323 | seqno = SeqNo, |
| 324 | timestamp = TS, |
| 325 | ssrc = Ssrc, |
| 326 | payload = Payload}. |
| 327 | |
| 328 | %%% payload generation %%% |
| 329 | |
| 330 | next_payload(F) when is_function(F) -> |
| 331 | {F(), F}; |
| 332 | next_payload({F, D}) when is_function(F) -> |
| 333 | {P, D2} = F(D), |
| 334 | {P, {F, D2}}; |
| 335 | next_payload([P | R]) -> |
| 336 | {P, R}; |
| 337 | next_payload([]) -> |
| 338 | undef; |
| 339 | next_payload(Bin = <<_/bytes>>) -> |
| 340 | {Bin, Bin}. |
| 341 | |
| 342 | %%% real writing work %%% |
| 343 | |
| 344 | write_packets(_Dev, DS, _P, _F, 0, _O, _Opts) -> |
| 345 | DS; |
| 346 | write_packets(Dev, DataSource, P = #rtp_packet{}, F, L, O, Opts) -> |
| 347 | Format = proplists:get_value(format, Opts, state), |
| 348 | Ptime = proplists:get_value(duration, Opts, 160), |
| 349 | Delay = proplists:get_value(delay, Opts, 0), |
Jacob Erlbeck | 8c43ce6 | 2014-03-14 18:06:23 +0100 | [diff] [blame] | 350 | Rate = proplists:get_value(rate, Opts, 8000), |
Jacob Erlbeck | da82df8 | 2013-12-13 13:18:20 +0100 | [diff] [blame] | 351 | case next_payload(DataSource) of |
| 352 | {Payload, DataSource2} -> |
Jacob Erlbeck | 8c43ce6 | 2014-03-14 18:06:23 +0100 | [diff] [blame] | 353 | write_packet(Dev, Ptime * F / Rate + Delay, |
Jacob Erlbeck | da82df8 | 2013-12-13 13:18:20 +0100 | [diff] [blame] | 354 | P#rtp_packet{seqno = F, timestamp = F*Ptime+O, |
| 355 | payload = Payload}, |
| 356 | Format), |
| 357 | write_packets(Dev, DataSource2, P, F+1, L-1, O, Opts); |
| 358 | Other -> Other |
| 359 | end. |
| 360 | |
| 361 | write_packet(Dev, Time, P = #rtp_packet{}, Format) -> |
| 362 | Bin = make_rtp_packet(P), |
| 363 | |
| 364 | write_packet_line(Dev, Time, P, Bin, Format). |
| 365 | |
| 366 | write_packet_pre(Dev, carray) -> |
| 367 | io:format(Dev, |
| 368 | "struct {float t; int len; char *data;} packets[] = {~n", []); |
| 369 | |
| 370 | write_packet_pre(_Dev, _) -> ok. |
| 371 | |
| 372 | write_packet_post(Dev, carray) -> |
| 373 | io:format(Dev, "};~n", []); |
| 374 | |
| 375 | write_packet_post(_Dev, _) -> ok. |
| 376 | |
| 377 | write_packet_line(Dev, Time, _P, Bin, state) -> |
| 378 | io:format(Dev, "~f ~s~n", [Time, bin_to_hex(Bin)]); |
| 379 | |
| 380 | write_packet_line(Dev, Time, #rtp_packet{seqno = N, timestamp = TS}, Bin, c) -> |
| 381 | ByteList = [ [ $0, $x | integer_to_list(Byte, 16) ] || <<Byte:8>> <= Bin ], |
| 382 | ByteStr = string:join(ByteList, ", "), |
| 383 | io:format(Dev, "/* time=~f, SeqNo=~B, TS=~B */ {~s}~n", [Time, N, TS, ByteStr]); |
| 384 | |
| 385 | write_packet_line(Dev, Time, #rtp_packet{seqno = N, timestamp = TS}, Bin, carray) -> |
| 386 | io:format(Dev, " /* RTP: SeqNo=~B, TS=~B */~n", [N, TS]), |
| 387 | io:format(Dev, " {~f, ~B, \"", [Time, size(Bin)]), |
| 388 | [ io:format(Dev, "\\x~2.16.0B", [Byte]) || <<Byte:8>> <= Bin ], |
| 389 | io:format(Dev, "\"},~n", []). |
| 390 | |
| 391 | %%% real reading work %%% |
| 392 | |
| 393 | read_packets(Dev, Opts) -> |
| 394 | Format = proplists:get_value(in_format, Opts, state), |
| 395 | |
| 396 | read_packets(Dev, Opts, Format). |
| 397 | |
| 398 | read_packets(Dev, Opts, Format) -> |
Jacob Erlbeck | 8c43ce6 | 2014-03-14 18:06:23 +0100 | [diff] [blame] | 399 | case read_packet(Dev, Opts, Format) of |
Jacob Erlbeck | da82df8 | 2013-12-13 13:18:20 +0100 | [diff] [blame] | 400 | eof -> []; |
| 401 | Tuple -> [Tuple | read_packets(Dev, Opts, Format)] |
| 402 | end. |
| 403 | |
Jacob Erlbeck | 8c43ce6 | 2014-03-14 18:06:23 +0100 | [diff] [blame] | 404 | read_packet(Dev, Opts, bin) -> |
| 405 | Size = proplists:get_value(frame_size, Opts), |
| 406 | case file:read(Dev, Size) of |
| 407 | {ok, Data} -> {0, #rtp_packet{payload = iolist_to_binary(Data)}}; |
| 408 | eof -> eof |
| 409 | end; |
| 410 | read_packet(Dev, _Opts, Format) -> |
Jacob Erlbeck | da82df8 | 2013-12-13 13:18:20 +0100 | [diff] [blame] | 411 | case read_packet_line(Dev, Format) of |
| 412 | {Time, Bin} -> {Time, parse_rtp_packet(Bin)}; |
| 413 | eof -> eof |
| 414 | end. |
| 415 | |
| 416 | read_packet_line(Dev, state) -> |
| 417 | case io:fread(Dev, "", "~f ~s") of |
| 418 | {ok, [Time, Hex]} -> {Time, hex_to_bin(Hex)}; |
| 419 | eof -> eof |
| 420 | end. |