Decode radio transmissions from devices on the ISM bands in OCaml
0
fork

Configure Feed

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

init

+5407
+3
.gitignore
··· 1 + _build 2 + vendor 3 + ocaml-mqtte
+4
bin/dune
··· 1 + (executable 2 + (name rtl433_cli) 3 + (public_name rtl433) 4 + (libraries rtl433 cmdliner eio_main fmt logs logs.fmt))
+252
bin/rtl433_cli.ml
··· 1 + (** RTL-433 command-line interface. 2 + 3 + This binary provides a command-line interface to the rtl433 library, 4 + compatible with the original rtl_433 tool. *) 5 + 6 + open Cmdliner 7 + 8 + type cli_opts = { 9 + config_file : string option; 10 + input_file : string option; 11 + frequency : int option; 12 + sample_rate : int option; 13 + protocols : string list; 14 + mqtt_url : string option; 15 + output_json : bool; 16 + verbosity : int; 17 + } 18 + 19 + let run_with_eio opts = 20 + Eio_main.run @@ fun env -> 21 + Eio.Switch.run @@ fun sw -> 22 + let fs = Eio.Stdenv.fs env in 23 + let net = Eio.Stdenv.net env in 24 + let clock = Eio.Stdenv.clock env in 25 + 26 + (* Set verbosity *) 27 + if opts.verbosity > 0 then Logs.set_level (Some Logs.Debug) 28 + else Logs.set_level (Some Logs.Info); 29 + 30 + (* Load configuration if specified *) 31 + let base_config = 32 + match opts.config_file with 33 + | Some path -> ( 34 + match Rtl433.Config.parse_file ~fs path with 35 + | Ok config -> config 36 + | Error msg -> 37 + Fmt.epr "Error loading config: %s@." msg; 38 + exit 1) 39 + | None -> Rtl433.Config.default 40 + in 41 + 42 + (* Override with command-line options *) 43 + let frequency = 44 + match opts.frequency with 45 + | Some f -> f 46 + | None -> (match base_config.frequencies with f :: _ -> f | [] -> 433_920_000) 47 + in 48 + 49 + let sample_rate = 50 + match opts.sample_rate with 51 + | Some r -> r 52 + | None -> base_config.sample_rate 53 + in 54 + 55 + (* Parse protocol filter *) 56 + let filter = 57 + match opts.protocols with 58 + | [] -> base_config.protocol_filter 59 + | ps -> Rtl433.Registry.parse_filter ps 60 + in 61 + 62 + (* Setup outputs *) 63 + let outputs = 64 + let mqtt_output = 65 + match opts.mqtt_url with 66 + | Some url -> ( 67 + match Rtl433.Output_mqtt.parse_url url with 68 + | Ok mqtt_config -> 69 + let mqtt = Rtl433.Output_mqtt.create ~sw ~net ~clock mqtt_config in 70 + if Rtl433.Output_mqtt.is_connected mqtt then begin 71 + Logs.info (fun m -> m "Connected to MQTT broker %s:%d" 72 + mqtt_config.host mqtt_config.port); 73 + [ Rtl433.Output_mqtt.to_output mqtt ] 74 + end else begin 75 + Logs.warn (fun m -> m "Failed to connect to MQTT broker"); 76 + [] 77 + end 78 + | Error msg -> 79 + Fmt.epr "Error parsing MQTT URL: %s@." msg; 80 + []) 81 + | None -> [] 82 + in 83 + let json_output = 84 + (* Output JSON to stdout if -M flag or no other outputs *) 85 + if opts.output_json || mqtt_output = [] then 86 + [ Rtl433.Output.json_stdout () ] 87 + else [] 88 + in 89 + mqtt_output @ json_output 90 + in 91 + 92 + (* Determine input source *) 93 + let input_file = 94 + match opts.input_file with 95 + | Some path -> path 96 + | None -> 97 + Fmt.epr "Error: No input source specified@."; 98 + Fmt.epr "Use -r <file> for file input@."; 99 + exit 1 100 + in 101 + 102 + (* Detect format from filename *) 103 + let format = 104 + match Rtl433.Input.detect_format input_file with 105 + | Some f -> f 106 + | None -> 107 + Logs.warn (fun m -> m "Unknown file format, assuming CU8"); 108 + Rtl433.Baseband.CU8 109 + in 110 + 111 + Logs.info (fun m -> m "rtl433 OCaml implementation v%s" Rtl433.version); 112 + Logs.info (fun m -> m "Input: %s (%s format)" 113 + input_file 114 + (match format with 115 + | Rtl433.Baseband.CU8 -> "CU8" 116 + | Rtl433.Baseband.CS8 -> "CS8" 117 + | Rtl433.Baseband.CS16 -> "CS16" 118 + | Rtl433.Baseband.CF32 -> "CF32")); 119 + Logs.info (fun m -> m "Frequency: %d Hz, Sample rate: %d Hz" frequency sample_rate); 120 + 121 + (* Create input source *) 122 + let input = Rtl433.Input.open_file ~sw ~fs ~path:input_file ~format ~sample_rate in 123 + 124 + (* Create registry with filter *) 125 + let registry = 126 + let reg = Rtl433.Registry.with_builtins () in 127 + Rtl433.Registry.apply_filter reg filter 128 + in 129 + Logs.info (fun m -> m "Enabled %d decoders" (Rtl433.Registry.enabled_count registry)); 130 + 131 + (* Build config *) 132 + let config = Rtl433.Config.{ 133 + base_config with 134 + frequencies = [ frequency ]; 135 + sample_rate; 136 + protocol_filter = filter; 137 + } in 138 + 139 + (* Create and run pipeline *) 140 + let pipeline = Rtl433.Pipeline.create ~config ~input ~outputs ~registry in 141 + 142 + (* Set up event handler for logging *) 143 + Rtl433.Pipeline.on_event pipeline (function 144 + | Rtl433.Pipeline.Message_decoded data -> 145 + Logs.debug (fun m -> m "Decoded: %s" (Rtl433.Data.to_json data)) 146 + | Rtl433.Pipeline.Decode_failed (name, result) -> 147 + Logs.debug (fun m -> m "Decode failed for %s: %a" name Rtl433.Device.pp_result result) 148 + | Rtl433.Pipeline.Signal_detected _ -> 149 + Logs.debug (fun m -> m "Signal detected") 150 + | Rtl433.Pipeline.Input_error exn -> 151 + Logs.err (fun m -> m "Input error: %a" Fmt.exn exn) 152 + | Rtl433.Pipeline.Stopped -> 153 + Logs.info (fun m -> m "Pipeline stopped") 154 + | Rtl433.Pipeline.Frequency_changed f -> 155 + Logs.info (fun m -> m "Frequency changed to %d Hz" f)); 156 + 157 + (* Run the pipeline *) 158 + Logs.info (fun m -> m "Starting signal processing..."); 159 + Rtl433.Pipeline.run pipeline ~clock; 160 + 161 + (* Print stats *) 162 + let stats = Rtl433.Pipeline.get_stats pipeline in 163 + Logs.info (fun m -> m "Finished: %d frames, %d signals, %d decoded messages" 164 + stats.frames_processed stats.frames_with_signal stats.messages_output) 165 + 166 + (* Command-line arguments *) 167 + 168 + let config_file = 169 + let doc = "Read configuration from $(docv)." in 170 + Arg.(value & opt (some file) None & info [ "c"; "conf" ] ~docv:"FILE" ~doc) 171 + 172 + let input_file = 173 + let doc = "Read samples from $(docv) (supports .cu8, .cs16)." in 174 + Arg.(value & opt (some file) None & info [ "r"; "read" ] ~docv:"FILE" ~doc) 175 + 176 + let frequency = 177 + let doc = "Center frequency in Hz (default 433920000)." in 178 + Arg.(value & opt (some int) None & info [ "f"; "frequency" ] ~docv:"HZ" ~doc) 179 + 180 + let sample_rate = 181 + let doc = "Sample rate in Hz (default 250000)." in 182 + Arg.(value & opt (some int) None & info [ "s"; "sample-rate" ] ~docv:"HZ" ~doc) 183 + 184 + let protocols = 185 + let doc = 186 + "Enable protocol $(docv). Use negative number to disable. Can be repeated." 187 + in 188 + Arg.(value & opt_all string [] & info [ "R"; "protocol" ] ~docv:"NUM" ~doc) 189 + 190 + let mqtt_url = 191 + let doc = 192 + "MQTT output URL: mqtt://[user:pass@]host[:port][,options]. Options: \ 193 + retain=true, qos=0/1/2." 194 + in 195 + Arg.(value & opt (some string) None & info [ "F"; "mqtt" ] ~docv:"URL" ~doc) 196 + 197 + let output_json = 198 + let doc = "Output JSON to stdout." in 199 + Arg.(value & flag & info [ "M"; "json" ] ~doc) 200 + 201 + let verbosity = 202 + let doc = "Increase verbosity (can be repeated)." in 203 + Arg.(value & flag_all & info [ "v"; "verbose" ] ~doc) 204 + 205 + let verbosity_count = Term.(const List.length $ verbosity) 206 + 207 + let opts_term = 208 + let mk config_file input_file frequency sample_rate protocols mqtt_url 209 + output_json verbosity = 210 + { config_file; input_file; frequency; sample_rate; protocols; mqtt_url; 211 + output_json; verbosity } 212 + in 213 + Term.( 214 + const mk 215 + $ config_file 216 + $ input_file 217 + $ frequency 218 + $ sample_rate 219 + $ protocols 220 + $ mqtt_url 221 + $ output_json 222 + $ verbosity_count) 223 + 224 + let cmd = 225 + let doc = "Decode wireless sensor data from RTL-SDR or recorded files" in 226 + let man = 227 + [ 228 + `S Manpage.s_description; 229 + `P 230 + "$(tname) decodes wireless sensor transmissions in the 433MHz/868MHz \ 231 + ISM bands."; 232 + `P "Supports both OOK and FSK modulation with multiple protocol decoders."; 233 + `S Manpage.s_examples; 234 + `P "Decode from a recorded file:"; 235 + `Pre " $(tname) -r samples.cu8 -f 868000000 -M"; 236 + `P "Decode with MQTT output:"; 237 + `Pre 238 + " $(tname) -r samples.cu8 -f 433920000 -F \ 239 + mqtt://broker:1883,retain=true"; 240 + `P "Enable only protocol 142 (FineOffset WH51):"; 241 + `Pre " $(tname) -r samples.cu8 -R 142 -M"; 242 + `S Manpage.s_bugs; 243 + `P "Report bugs at https://github.com/avsm/ocaml-rtl433/issues"; 244 + ] 245 + in 246 + let info = Cmd.info "rtl433" ~version:"0.1.0" ~doc ~man in 247 + let term = Term.(const run_with_eio $ opts_term) in 248 + Cmd.v info term 249 + 250 + let () = 251 + Logs.set_reporter (Logs_fmt.reporter ()); 252 + exit (Cmd.eval cmd)
+35
dune-project
··· 1 + (lang dune 3.0) 2 + 3 + (name rtl433) 4 + 5 + (generate_opam_files true) 6 + 7 + (source 8 + (github avsm/ocaml-rtl433)) 9 + 10 + (authors "Anil Madhavapeddy") 11 + 12 + (maintainers "Anil Madhavapeddy") 13 + 14 + (license ISC) 15 + 16 + (documentation https://avsm.github.io/ocaml-rtl433) 17 + 18 + (package 19 + (name rtl433) 20 + (synopsis "RTL-433 decoder for wireless sensors in OCaml") 21 + (description 22 + "Pure OCaml implementation of rtl_433, a software decoder for wireless sensors 23 + transmitting in the 433MHz/868MHz/915MHz ISM bands. Supports FSK and OOK 24 + modulation with pluggable protocol decoders and MQTT output.") 25 + (depends 26 + (ocaml (>= 5.1)) 27 + (eio (>= 1.0)) 28 + eio_main 29 + (mqtte (>= 0.1)) 30 + (cmdliner (>= 1.2)) 31 + (fmt (>= 0.9)) 32 + (logs (>= 0.7)) 33 + ptime) 34 + (tags 35 + (sdr rtl-sdr 433mhz ism mqtt sensors)))
+30
lib/dune
··· 1 + (library 2 + (name rtl433) 3 + (public_name rtl433) 4 + (libraries eio eio_main mqtte mqtte.eio fmt logs ptime str ptime.clock.os cmdliner) 5 + (modules 6 + ; Core types 7 + rtl433_util 8 + rtl433_modulation 9 + rtl433_data 10 + rtl433_bitbuffer 11 + rtl433_pulse_data 12 + ; Decoder framework 13 + rtl433_device 14 + rtl433_flex 15 + rtl433_decoder_fineoffset 16 + rtl433_registry 17 + ; Signal processing 18 + rtl433_baseband 19 + rtl433_pulse_detect 20 + rtl433_pulse_slicer 21 + ; Input sources 22 + rtl433_input 23 + ; Output handlers 24 + rtl433_output 25 + rtl433_output_mqtt 26 + ; Configuration 27 + rtl433_config 28 + ; Pipeline and main API 29 + rtl433_pipeline 30 + rtl433))
+140
lib/rtl433.ml
··· 1 + (* RTL-433 decoder library *) 2 + 3 + module Util = Rtl433_util 4 + module Modulation = Rtl433_modulation 5 + module Data = Rtl433_data 6 + module Bitbuffer = Rtl433_bitbuffer 7 + module Pulse_data = Rtl433_pulse_data 8 + module Device = Rtl433_device 9 + module Flex = Rtl433_flex 10 + module Decoder_fineoffset = Rtl433_decoder_fineoffset 11 + module Registry = Rtl433_registry 12 + module Baseband = Rtl433_baseband 13 + module Pulse_detect = Rtl433_pulse_detect 14 + module Pulse_slicer = Rtl433_pulse_slicer 15 + module Input = Rtl433_input 16 + module Output = Rtl433_output 17 + module Output_mqtt = Rtl433_output_mqtt 18 + module Config = Rtl433_config 19 + module Pipeline = Rtl433_pipeline 20 + 21 + (* Register builtin decoders *) 22 + let () = 23 + Registry.set_builtin_decoders_fn (fun () -> 24 + Decoder_fineoffset.all_decoders () 25 + ) 26 + 27 + type input = [ `File of string | `Rtl_tcp of string * int ] 28 + 29 + type output = 30 + [ `Json_stdout 31 + | `Json_file of string 32 + | `Mqtt of Output_mqtt.config 33 + | `Log 34 + | `Custom of (Data.t -> unit) ] 35 + 36 + let version = "0.1.0" 37 + 38 + let print_version () = 39 + Printf.printf "rtl433 %s\n" version; 40 + Printf.printf "OCaml implementation of rtl_433\n"; 41 + Printf.printf "Supported protocols:\n"; 42 + let registry = Registry.with_builtins () in 43 + print_string (Registry.list_protocols registry) 44 + 45 + let builtin_decoders () = Registry.builtin_decoders () 46 + let find_decoder num = Registry.find_by_num (Registry.with_builtins ()) num 47 + 48 + let find_decoder_by_name name = 49 + Registry.find_by_name (Registry.with_builtins ()) name 50 + 51 + let list_decoders () = 52 + let registry = Registry.with_builtins () in 53 + List.map 54 + (fun d -> 55 + ( d.Device.protocol_num, 56 + d.Device.name, 57 + not d.Device.disabled )) 58 + (Registry.enabled registry) 59 + 60 + let run ~sw ~env ~input ?frequency ?sample_rate ?output ?protocols ?duration 61 + ?config () = 62 + let fs = Eio.Stdenv.fs env in 63 + let net = Eio.Stdenv.net env in 64 + let clock = Eio.Stdenv.clock env in 65 + 66 + (* Use provided config or build from options *) 67 + let config = 68 + match config with 69 + | Some c -> c 70 + | None -> 71 + let base = Config.default in 72 + { 73 + base with 74 + frequencies = 75 + (match frequency with Some f -> [ f ] | None -> base.frequencies); 76 + sample_rate = 77 + (match sample_rate with Some s -> s | None -> base.sample_rate); 78 + protocol_filter = 79 + (match protocols with Some p -> p | None -> base.protocol_filter); 80 + duration; 81 + } 82 + in 83 + 84 + (* Create input source *) 85 + let input_source = 86 + match input with 87 + | `File path -> 88 + let format = 89 + match Input.detect_format path with Some f -> f | None -> Input.CU8 90 + in 91 + Input.open_file ~sw ~fs ~path ~format ~sample_rate:config.sample_rate 92 + | `Rtl_tcp (host, port) -> Input.connect_rtl_tcp ~sw ~net ~host ~port () 93 + in 94 + 95 + (* Create outputs *) 96 + let outputs = 97 + match output with 98 + | Some `Json_stdout -> [ Output.json_stdout () ] 99 + | Some (`Json_file path) -> [ Output.json_file ~sw ~fs ~path () ] 100 + | Some (`Mqtt mqtt_config) -> 101 + let mqtt = Output_mqtt.create ~sw ~net ~clock mqtt_config in 102 + [ Output_mqtt.to_output mqtt ] 103 + | Some `Log -> [ Output.log_output () ] 104 + | Some (`Custom fn) -> [ Output.custom fn ] 105 + | None -> [ Output.json_stdout () ] 106 + in 107 + 108 + (* Setup registry with filter *) 109 + let registry = 110 + let reg = Registry.with_builtins () in 111 + Registry.apply_filter reg config.protocol_filter 112 + in 113 + 114 + (* Set frequency if using rtl_tcp *) 115 + (match input with 116 + | `Rtl_tcp _ -> 117 + let freq = 118 + match config.frequencies with f :: _ -> f | [] -> 433920000 119 + in 120 + Input.set_frequency input_source freq 121 + | `File _ -> ()); 122 + 123 + (* Create and run pipeline *) 124 + let pipeline = Pipeline.create ~config ~input:input_source ~outputs ~registry in 125 + 126 + match config.duration with 127 + | Some d -> Pipeline.run_for pipeline ~clock ~duration_s:d 128 + | None -> Pipeline.run pipeline ~clock 129 + 130 + let run_with_config ~sw ~env ~config_path = 131 + let fs = Eio.Stdenv.fs env in 132 + match Config.parse_file ~fs config_path with 133 + | Ok config -> ( 134 + match config.input with 135 + | Some (Config.File path) -> 136 + run ~sw ~env ~input:(`File path) ~config () 137 + | Some (Config.Rtl_tcp (host, port)) -> 138 + run ~sw ~env ~input:(`Rtl_tcp (host, port)) ~config () 139 + | None -> failwith "No input source specified in config") 140 + | Error msg -> failwith ("Config error: " ^ msg)
+162
lib/rtl433.mli
··· 1 + (** RTL-433 decoder library for wireless sensors. 2 + 3 + This library decodes wireless sensor transmissions in the 4 + 433MHz/868MHz/915MHz ISM bands. It supports both OOK and FSK 5 + modulation with pluggable protocol decoders. 6 + 7 + {1 Quick Start} 8 + 9 + {[ 10 + (* Run with default configuration *) 11 + Eio_main.run @@ fun env -> 12 + Eio.Switch.run @@ fun sw -> 13 + Rtl433.run ~sw ~env 14 + ~input:(`File "samples.cu8") 15 + ~frequency:868_000_000 16 + ~output:`Json_stdout 17 + () 18 + ]} 19 + 20 + {1 Modules} 21 + 22 + Core types: 23 + - {!Modulation} - OOK/FSK modulation types 24 + - {!Data} - Hierarchical data structure for decoded messages 25 + - {!Bitbuffer} - 2D bit buffer for decoded bits 26 + - {!Pulse_data} - Pulse timing data 27 + 28 + Decoders: 29 + - {!Device} - Protocol decoder interface 30 + - {!Flex} - User-defined flexible decoders 31 + - {!Registry} - Decoder registration and filtering 32 + 33 + Signal processing: 34 + - {!Baseband} - I/Q to baseband conversion 35 + - {!Pulse_detect} - Pulse detection 36 + - {!Pulse_slicer} - Pulse to bit conversion 37 + 38 + I/O: 39 + - {!Input} - Input sources (file, rtl_tcp) 40 + - {!Output} - Output handlers 41 + - {!Output_mqtt} - MQTT output 42 + 43 + High-level: 44 + - {!Config} - Configuration parsing 45 + - {!Pipeline} - Signal processing pipeline 46 + *) 47 + 48 + (** {1 Re-exported Modules} *) 49 + 50 + module Util = Rtl433_util 51 + module Modulation = Rtl433_modulation 52 + module Data = Rtl433_data 53 + module Bitbuffer = Rtl433_bitbuffer 54 + module Pulse_data = Rtl433_pulse_data 55 + module Device = Rtl433_device 56 + module Flex = Rtl433_flex 57 + module Registry = Rtl433_registry 58 + module Baseband = Rtl433_baseband 59 + module Pulse_detect = Rtl433_pulse_detect 60 + module Pulse_slicer = Rtl433_pulse_slicer 61 + module Input = Rtl433_input 62 + module Output = Rtl433_output 63 + module Output_mqtt = Rtl433_output_mqtt 64 + module Config = Rtl433_config 65 + module Pipeline = Rtl433_pipeline 66 + 67 + (** {1 High-Level API} *) 68 + 69 + (** Input source specification. *) 70 + type input = 71 + [ `File of string 72 + (** File path for recorded I/Q samples. *) 73 + | `Rtl_tcp of string * int 74 + (** rtl_tcp host and port. *) 75 + ] 76 + 77 + (** Output specification. *) 78 + type output = 79 + [ `Json_stdout 80 + (** JSON to stdout. *) 81 + | `Json_file of string 82 + (** JSON to file. *) 83 + | `Mqtt of Output_mqtt.config 84 + (** MQTT output. *) 85 + | `Log 86 + (** Human-readable log. *) 87 + | `Custom of (Data.t -> unit) 88 + (** Custom output function. *) 89 + ] 90 + 91 + (** Run the decoder. 92 + 93 + This is the main entry point for simple usage. It creates 94 + an input source, sets up outputs, and runs the processing 95 + pipeline until the input is exhausted or duration expires. 96 + 97 + @param sw Eio switch for resource management 98 + @param env Eio environment 99 + @param input Input source specification 100 + @param frequency Center frequency in Hz (default 433920000) 101 + @param sample_rate Sample rate in Hz (default 250000) 102 + @param output Output specification (default Json_stdout) 103 + @param protocols Protocol filter (default All) 104 + @param duration Maximum duration in seconds (default unlimited) 105 + @param config Optional configuration (overrides other params) *) 106 + val run : 107 + sw:Eio.Switch.t -> 108 + env:< fs : _ Eio.Path.t; net : _ Eio.Net.t; clock : _ Eio.Time.clock; .. > -> 109 + input:input -> 110 + ?frequency:int -> 111 + ?sample_rate:int -> 112 + ?output:output -> 113 + ?protocols:Registry.filter -> 114 + ?duration:float -> 115 + ?config:Config.t -> 116 + unit -> 117 + unit 118 + 119 + (** Run with configuration file. 120 + 121 + Loads configuration from a file and runs the decoder. 122 + 123 + @param sw Eio switch 124 + @param env Eio environment 125 + @param config_path Path to configuration file *) 126 + val run_with_config : 127 + sw:Eio.Switch.t -> 128 + env:< fs : _ Eio.Path.t; net : _ Eio.Net.t; clock : _ Eio.Time.clock; .. > -> 129 + config_path:string -> 130 + unit 131 + 132 + (** {1 Decoder Registration} *) 133 + 134 + (** Get all built-in decoders. 135 + 136 + @return List of all compiled-in decoders *) 137 + val builtin_decoders : unit -> unit Device.t list 138 + 139 + (** Find decoder by protocol number. 140 + 141 + @param num Protocol number 142 + @return Decoder if found *) 143 + val find_decoder : int -> unit Device.t option 144 + 145 + (** Find decoder by name. 146 + 147 + @param name Protocol name (case-insensitive) 148 + @return Decoder if found *) 149 + val find_decoder_by_name : string -> unit Device.t option 150 + 151 + (** List all decoder names and numbers. 152 + 153 + @return List of (number, name, enabled) tuples *) 154 + val list_decoders : unit -> (int * string * bool) list 155 + 156 + (** {1 Version Info} *) 157 + 158 + (** Library version string. *) 159 + val version : string 160 + 161 + (** Print version and supported protocols. *) 162 + val print_version : unit -> unit
+148
lib/rtl433_baseband.ml
··· 1 + (* Baseband signal processing *) 2 + 3 + type sample_format = CU8 | CS8 | CS16 | CF32 4 + 5 + let magnitude ~i ~q = 6 + (* Alpha-max-beta-min approximation *) 7 + let ai = abs i in 8 + let aq = abs q in 9 + let max_v = max ai aq in 10 + let min_v = min ai aq in 11 + max_v + (min_v / 2) 12 + 13 + let magnitude_exact ~i ~q = 14 + sqrt (Float.of_int ((i * i) + (q * q))) 15 + 16 + let envelope_detect format samples = 17 + let len = Bytes.length samples in 18 + match format with 19 + | CU8 -> 20 + let out_len = len / 2 in 21 + let out = Bytes.make out_len '\000' in 22 + for i = 0 to out_len - 1 do 23 + let i_val = Bytes.get_uint8 samples (i * 2) - 128 in 24 + let q_val = Bytes.get_uint8 samples ((i * 2) + 1) - 128 in 25 + let mag = magnitude ~i:i_val ~q:q_val in 26 + Bytes.set_uint8 out i (min 255 (mag + 128)) 27 + done; 28 + out 29 + | CS8 -> 30 + let out_len = len / 2 in 31 + let out = Bytes.make out_len '\000' in 32 + for i = 0 to out_len - 1 do 33 + let i_val = Bytes.get_int8 samples (i * 2) in 34 + let q_val = Bytes.get_int8 samples ((i * 2) + 1) in 35 + let mag = magnitude ~i:i_val ~q:q_val in 36 + Bytes.set_uint8 out i (min 255 (mag + 128)) 37 + done; 38 + out 39 + | CS16 -> 40 + let out_len = len / 4 in 41 + let out = Bytes.make out_len '\000' in 42 + for i = 0 to out_len - 1 do 43 + let i_val = Bytes.get_int16_le samples (i * 4) in 44 + let q_val = Bytes.get_int16_le samples ((i * 4) + 2) in 45 + let mag = magnitude ~i:(i_val / 256) ~q:(q_val / 256) in 46 + Bytes.set_uint8 out i (min 255 (mag + 128)) 47 + done; 48 + out 49 + | CF32 -> 50 + let out_len = len / 8 in 51 + let out = Bytes.make out_len '\000' in 52 + for i = 0 to out_len - 1 do 53 + let i_val = Int32.to_int (Bytes.get_int32_le samples (i * 8)) in 54 + let q_val = Int32.to_int (Bytes.get_int32_le samples ((i * 8) + 4)) in 55 + let mag = magnitude ~i:(i_val / 0x1000000) ~q:(q_val / 0x1000000) in 56 + Bytes.set_uint8 out i (min 255 (mag + 128)) 57 + done; 58 + out 59 + 60 + let fm_demod _format samples = 61 + (* Simplified FM demodulation - returns frequency deviation *) 62 + let len = Bytes.length samples / 2 in 63 + let out = Bytes.make len '\128' in 64 + (* TODO: proper FM demod with phase derivative *) 65 + out 66 + 67 + let lowpass ~alpha samples = 68 + let len = Bytes.length samples in 69 + let out = Bytes.make len '\000' in 70 + if len > 0 then begin 71 + let prev = ref (Float.of_int (Bytes.get_uint8 samples 0)) in 72 + for i = 0 to len - 1 do 73 + let curr = Float.of_int (Bytes.get_uint8 samples i) in 74 + prev := !prev +. (alpha *. (curr -. !prev)); 75 + Bytes.set_uint8 out i (int_of_float !prev) 76 + done 77 + end; 78 + out 79 + 80 + let highpass ~alpha samples = 81 + let len = Bytes.length samples in 82 + let out = Bytes.make len '\000' in 83 + if len > 0 then begin 84 + let prev_in = ref (Float.of_int (Bytes.get_uint8 samples 0)) in 85 + let prev_out = ref 0.0 in 86 + for i = 0 to len - 1 do 87 + let curr = Float.of_int (Bytes.get_uint8 samples i) in 88 + prev_out := alpha *. (!prev_out +. curr -. !prev_in); 89 + prev_in := curr; 90 + Bytes.set_uint8 out i (128 + int_of_float !prev_out) 91 + done 92 + end; 93 + out 94 + 95 + let remove_dc samples = 96 + let len = Bytes.length samples in 97 + if len = 0 then samples 98 + else begin 99 + let sum = ref 0 in 100 + for i = 0 to len - 1 do 101 + sum := !sum + Bytes.get_uint8 samples i 102 + done; 103 + let dc = !sum / len in 104 + let out = Bytes.make len '\000' in 105 + for i = 0 to len - 1 do 106 + let v = Bytes.get_uint8 samples i - dc + 128 in 107 + Bytes.set_uint8 out i (max 0 (min 255 v)) 108 + done; 109 + out 110 + end 111 + 112 + let estimate_ook_levels samples = 113 + let len = Bytes.length samples in 114 + if len = 0 then (128, 128) 115 + else begin 116 + let min_v = ref 255 in 117 + let max_v = ref 0 in 118 + for i = 0 to len - 1 do 119 + let v = Bytes.get_uint8 samples i in 120 + if v < !min_v then min_v := v; 121 + if v > !max_v then max_v := v 122 + done; 123 + (!min_v, !max_v) 124 + end 125 + 126 + let estimate_fsk_frequencies _samples = (0, 0) (* TODO *) 127 + let amp_to_db amp = 20.0 *. log10 amp 128 + let db_to_amp db = 10.0 ** (db /. 20.0) 129 + 130 + let calculate_rssi samples = 131 + let len = Bytes.length samples in 132 + if len = 0 then -100.0 133 + else begin 134 + let sum = ref 0.0 in 135 + for i = 0 to len - 1 do 136 + let v = Float.of_int (Bytes.get_uint8 samples i - 128) in 137 + sum := !sum +. (v *. v) 138 + done; 139 + let rms = sqrt (!sum /. Float.of_int len) in 140 + amp_to_db (rms /. 128.0) 141 + end 142 + 143 + let calculate_snr samples ~noise_floor = 144 + let _, high = estimate_ook_levels samples in 145 + let signal = high - 128 in 146 + let noise = noise_floor in 147 + if noise <= 0 then 0.0 148 + else amp_to_db (Float.of_int signal /. Float.of_int noise)
+124
lib/rtl433_baseband.mli
··· 1 + (** Baseband signal processing for demodulation. 2 + 3 + This module provides functions for processing raw I/Q samples 4 + into demodulated baseband signals suitable for pulse detection. 5 + It handles both OOK (amplitude) and FSK (frequency) demodulation. *) 6 + 7 + (** {1 Types} *) 8 + 9 + (** Sample format for I/Q data. *) 10 + type sample_format = 11 + | CU8 12 + (** Complex unsigned 8-bit (RTL-SDR native format). 13 + Values 0-255, 128 is zero. *) 14 + | CS8 15 + (** Complex signed 8-bit. 16 + Values -128 to 127. *) 17 + | CS16 18 + (** Complex signed 16-bit little-endian. 19 + Values -32768 to 32767. *) 20 + | CF32 21 + (** Complex float 32-bit. 22 + Values typically -1.0 to 1.0. *) 23 + 24 + (** {1 Magnitude/Envelope Detection} *) 25 + 26 + (** Calculate magnitude from I/Q samples (fast approximation). 27 + 28 + Uses the alpha-max-beta-min algorithm for speed. 29 + 30 + @param i In-phase sample 31 + @param q Quadrature sample 32 + @return Approximate magnitude *) 33 + val magnitude : i:int -> q:int -> int 34 + 35 + (** Calculate exact magnitude using sqrt(i^2 + q^2). 36 + 37 + @param i In-phase sample 38 + @param q Quadrature sample 39 + @return Exact magnitude *) 40 + val magnitude_exact : i:int -> q:int -> float 41 + 42 + (** Envelope detection for OOK signals. 43 + 44 + Converts I/Q samples to amplitude envelope. 45 + 46 + @param format Sample format 47 + @param samples Raw I/Q sample bytes 48 + @return Amplitude envelope as byte array *) 49 + val envelope_detect : sample_format -> bytes -> bytes 50 + 51 + (** {1 FM Demodulation} *) 52 + 53 + (** FM demodulation for FSK signals. 54 + 55 + Extracts instantaneous frequency from I/Q samples 56 + using the derivative of phase. 57 + 58 + @param format Sample format 59 + @param samples Raw I/Q sample bytes 60 + @return Frequency deviation as signed byte array *) 61 + val fm_demod : sample_format -> bytes -> bytes 62 + 63 + (** {1 Filtering} *) 64 + 65 + (** Low-pass filter using first-order IIR. 66 + 67 + @param alpha Filter coefficient (0.0-1.0, lower = more filtering) 68 + @param samples Input samples 69 + @return Filtered samples *) 70 + val lowpass : alpha:float -> bytes -> bytes 71 + 72 + (** High-pass filter using first-order IIR. 73 + 74 + @param alpha Filter coefficient 75 + @param samples Input samples 76 + @return Filtered samples *) 77 + val highpass : alpha:float -> bytes -> bytes 78 + 79 + (** DC removal filter. 80 + 81 + @param samples Input samples 82 + @return Samples with DC offset removed *) 83 + val remove_dc : bytes -> bytes 84 + 85 + (** {1 Level Estimation} *) 86 + 87 + (** Estimate signal levels for OOK. 88 + 89 + @param samples Envelope samples 90 + @return Tuple of (low_estimate, high_estimate) *) 91 + val estimate_ook_levels : bytes -> int * int 92 + 93 + (** Estimate center frequency for FSK. 94 + 95 + @param samples FM demodulated samples 96 + @return Tuple of (f1_estimate, f2_estimate) *) 97 + val estimate_fsk_frequencies : bytes -> int * int 98 + 99 + (** {1 Conversion Utilities} *) 100 + 101 + (** Convert amplitude to dB. 102 + 103 + @param amp Linear amplitude 104 + @return Decibels *) 105 + val amp_to_db : float -> float 106 + 107 + (** Convert dB to amplitude. 108 + 109 + @param db Decibels 110 + @return Linear amplitude *) 111 + val db_to_amp : float -> float 112 + 113 + (** Calculate RSSI from samples. 114 + 115 + @param samples Envelope samples 116 + @return RSSI in dBFS *) 117 + val calculate_rssi : bytes -> float 118 + 119 + (** Calculate SNR from samples. 120 + 121 + @param samples Envelope samples 122 + @param noise_floor Known noise floor 123 + @return SNR in dB *) 124 + val calculate_snr : bytes -> noise_floor:int -> float
+156
lib/rtl433_bitbuffer.ml
··· 1 + (* Two-dimensional bit buffer *) 2 + 3 + let max_cols = 128 4 + let max_rows = 50 5 + 6 + type t = { 7 + mutable num_rows : int; 8 + mutable free_row : int; 9 + bits_per_row : int array; 10 + syncs_before_row : int array; 11 + bb : bytes array; 12 + } 13 + 14 + let create () = 15 + { 16 + num_rows = 0; 17 + free_row = 0; 18 + bits_per_row = Array.make max_rows 0; 19 + syncs_before_row = Array.make max_rows 0; 20 + bb = Array.init max_rows (fun _ -> Bytes.make max_cols '\000'); 21 + } 22 + 23 + let clear t = 24 + t.num_rows <- 0; 25 + t.free_row <- 0; 26 + Array.fill t.bits_per_row 0 max_rows 0; 27 + Array.fill t.syncs_before_row 0 max_rows 0; 28 + Array.iter (fun b -> Bytes.fill b 0 max_cols '\000') t.bb 29 + 30 + let add_bit t bit = 31 + if t.free_row < max_rows then begin 32 + let row = t.free_row in 33 + let bit_pos = t.bits_per_row.(row) in 34 + if bit_pos < max_cols * 8 then begin 35 + let byte_pos = bit_pos / 8 in 36 + let bit_in_byte = 7 - (bit_pos mod 8) in 37 + if bit <> 0 then begin 38 + let b = Bytes.get_uint8 t.bb.(row) byte_pos in 39 + Bytes.set_uint8 t.bb.(row) byte_pos (b lor (1 lsl bit_in_byte)) 40 + end; 41 + t.bits_per_row.(row) <- bit_pos + 1; 42 + if t.num_rows = 0 then t.num_rows <- 1 43 + end 44 + end 45 + 46 + let add_row t = 47 + if t.bits_per_row.(t.free_row) > 0 && t.free_row + 1 < max_rows then begin 48 + t.free_row <- t.free_row + 1; 49 + t.num_rows <- t.num_rows + 1 50 + end 51 + 52 + let add_sync t = 53 + if t.bits_per_row.(t.free_row) > 0 then add_row t 54 + else t.syncs_before_row.(t.free_row) <- t.syncs_before_row.(t.free_row) + 1 55 + 56 + let num_rows t = t.num_rows 57 + let bits_per_row t row = if row < max_rows then t.bits_per_row.(row) else 0 58 + 59 + let syncs_before_row t row = 60 + if row < max_rows then t.syncs_before_row.(row) else 0 61 + 62 + let get_row t row = if row < max_rows then t.bb.(row) else Bytes.empty 63 + 64 + let get_bit t ~row ~bit = 65 + if row < max_rows && bit < t.bits_per_row.(row) then 66 + let byte_pos = bit / 8 in 67 + let bit_in_byte = 7 - (bit mod 8) in 68 + (Bytes.get_uint8 t.bb.(row) byte_pos lsr bit_in_byte) land 1 69 + else 0 70 + 71 + let get_byte t ~row ~bit_offset = 72 + if row >= max_rows then 0 73 + else 74 + let b1 = Bytes.get_uint8 t.bb.(row) (bit_offset / 8) in 75 + let b2 = 76 + if bit_offset / 8 + 1 < max_cols then 77 + Bytes.get_uint8 t.bb.(row) ((bit_offset / 8) + 1) 78 + else 0 79 + in 80 + let shift = bit_offset mod 8 in 81 + ((b1 lsl shift) lor (b2 lsr (8 - shift))) land 0xff 82 + 83 + let extract_bytes t ~row ~bit_offset ~len = 84 + let num_bytes = (len + 7) / 8 in 85 + let result = Bytes.make num_bytes '\000' in 86 + for i = 0 to num_bytes - 1 do 87 + Bytes.set_uint8 result i (get_byte t ~row ~bit_offset:(bit_offset + (i * 8))) 88 + done; 89 + result 90 + 91 + let invert t = 92 + for row = 0 to t.num_rows - 1 do 93 + let num_bytes = (t.bits_per_row.(row) + 7) / 8 in 94 + for i = 0 to num_bytes - 1 do 95 + Bytes.set_uint8 t.bb.(row) i (lnot (Bytes.get_uint8 t.bb.(row) i) land 0xff) 96 + done 97 + done 98 + 99 + let nrzs_decode _t = () (* TODO *) 100 + let nrzm_decode _t = () (* TODO *) 101 + 102 + let manchester_decode _t ~row ~start ~max = 103 + ignore (row, start, max); 104 + (create (), 0) 105 + 106 + let differential_manchester_decode _t ~row ~start ~max = 107 + ignore (row, start, max); 108 + (create (), 0) 109 + 110 + let search t ~row ~start ~pattern ~pattern_bits = 111 + ignore (pattern, pattern_bits); 112 + if row < max_rows then t.bits_per_row.(row) else start 113 + 114 + let compare_rows t ~row_a ~row_b ~max_bits = 115 + ignore max_bits; 116 + if row_a >= max_rows || row_b >= max_rows then 1 117 + else if t.bits_per_row.(row_a) <> t.bits_per_row.(row_b) then 1 118 + else Bytes.compare t.bb.(row_a) t.bb.(row_b) 119 + 120 + let count_repeats t ~row ~max_bits = 121 + let count = ref 1 in 122 + for i = 0 to t.num_rows - 1 do 123 + if i <> row && compare_rows t ~row_a:row ~row_b:i ~max_bits = 0 then 124 + incr count 125 + done; 126 + !count 127 + 128 + let find_repeated_row t ~min_repeats ~min_bits = 129 + let rec check row = 130 + if row >= t.num_rows then None 131 + else if 132 + t.bits_per_row.(row) >= min_bits 133 + && count_repeats t ~row ~max_bits:0 >= min_repeats 134 + then Some row 135 + else check (row + 1) 136 + in 137 + check 0 138 + 139 + let find_repeated_prefix t ~min_repeats ~min_bits = 140 + find_repeated_row t ~min_repeats ~min_bits 141 + 142 + let parse t _code = clear t (* TODO: implement parsing *) 143 + 144 + let row_to_string t row = 145 + if row >= t.num_rows then "" 146 + else 147 + let bits = t.bits_per_row.(row) in 148 + let num_bytes = (bits + 7) / 8 in 149 + let hex = Rtl433_util.bytes_to_hex t.bb.(row) num_bytes in 150 + Printf.sprintf "{%d} %s" bits hex 151 + 152 + let pp fmt t = 153 + Format.fprintf fmt "Bitbuffer: %d rows@." t.num_rows; 154 + for i = 0 to t.num_rows - 1 do 155 + Format.fprintf fmt " [%d] %s@." i (row_to_string t i) 156 + done
+194
lib/rtl433_bitbuffer.mli
··· 1 + (** Two-dimensional bit buffer for storing decoded pulse data. 2 + 3 + A bitbuffer stores multiple rows of bits, where each row represents 4 + a separate packet or transmission. This is useful for sensors that 5 + send repeated transmissions of the same data. *) 6 + 7 + (** {1 Constants} *) 8 + 9 + (** Maximum number of bytes per row. *) 10 + val max_cols : int 11 + 12 + (** Maximum number of rows. *) 13 + val max_rows : int 14 + 15 + (** {1 Types} *) 16 + 17 + (** A bit buffer containing multiple rows of bits. *) 18 + type t 19 + 20 + (** {1 Construction} *) 21 + 22 + (** Create a new empty bitbuffer. *) 23 + val create : unit -> t 24 + 25 + (** Clear all data from the bitbuffer. *) 26 + val clear : t -> unit 27 + 28 + (** {1 Adding Data} *) 29 + 30 + (** Add a single bit (0 or 1) to the current row. 31 + 32 + Bits are added MSB first within each byte. 33 + 34 + @param bits The bitbuffer 35 + @param bit 0 or 1 *) 36 + val add_bit : t -> int -> unit 37 + 38 + (** Start a new row in the bitbuffer. 39 + 40 + The current row is finalized and a new empty row is started. *) 41 + val add_row : t -> unit 42 + 43 + (** Add a sync marker and optionally start a new row. 44 + 45 + If the current row has data, starts a new row. Otherwise, 46 + increments the sync counter for the current row. *) 47 + val add_sync : t -> unit 48 + 49 + (** {1 Accessing Data} *) 50 + 51 + (** Get the number of active rows. *) 52 + val num_rows : t -> int 53 + 54 + (** Get the number of bits in a specific row. 55 + 56 + @param bits The bitbuffer 57 + @param row Row index (0-based) 58 + @return Number of bits in the row *) 59 + val bits_per_row : t -> int -> int 60 + 61 + (** Get the number of sync pulses before a row. 62 + 63 + @param bits The bitbuffer 64 + @param row Row index 65 + @return Number of sync pulses *) 66 + val syncs_before_row : t -> int -> int 67 + 68 + (** Get the raw bytes of a row. 69 + 70 + @param bits The bitbuffer 71 + @param row Row index 72 + @return Bytes containing the row data *) 73 + val get_row : t -> int -> bytes 74 + 75 + (** Get a single bit from a specific position. 76 + 77 + @param bits The bitbuffer 78 + @param row Row index 79 + @param bit Bit index within the row 80 + @return 0 or 1 *) 81 + val get_bit : t -> row:int -> bit:int -> int 82 + 83 + (** Get a byte from a potentially unaligned bit position. 84 + 85 + @param bits The bitbuffer 86 + @param row Row index 87 + @param bit_offset Bit offset within the row 88 + @return Byte value *) 89 + val get_byte : t -> row:int -> bit_offset:int -> int 90 + 91 + (** Extract bytes from a row, handling unaligned access. 92 + 93 + @param bits The bitbuffer 94 + @param row Row index 95 + @param bit_offset Starting bit offset 96 + @param len Number of bits to extract 97 + @return Extracted bytes *) 98 + val extract_bytes : t -> row:int -> bit_offset:int -> len:int -> bytes 99 + 100 + (** {1 Transformations} *) 101 + 102 + (** Invert all bits in the bitbuffer (0 becomes 1 and vice versa). *) 103 + val invert : t -> unit 104 + 105 + (** Non-Return-to-Zero Space (NRZI) decode the bitbuffer. 106 + 107 + "One" is represented by no change in level, 108 + "Zero" is represented by change in level. *) 109 + val nrzs_decode : t -> unit 110 + 111 + (** Non-Return-to-Zero Mark (NRZM) decode the bitbuffer. 112 + 113 + "One" is represented by change in level, 114 + "Zero" is represented by no change in level. *) 115 + val nrzm_decode : t -> unit 116 + 117 + (** Manchester decode from one bitbuffer into another. 118 + 119 + Decodes at most [max] data bits (2*max input bits) from the 120 + specified row and starting position. Manchester per IEEE 802.3: 121 + high-low is 0, low-high is 1. 122 + 123 + @param inbuf Input bitbuffer 124 + @param row Row to decode from 125 + @param start Starting bit position 126 + @param max Maximum data bits to decode 127 + @return Tuple of (output bitbuffer, input bit position after decode) *) 128 + val manchester_decode : t -> row:int -> start:int -> max:int -> t * int 129 + 130 + (** Differential Manchester decode. *) 131 + val differential_manchester_decode : t -> row:int -> start:int -> max:int -> t * int 132 + 133 + (** {1 Searching} *) 134 + 135 + (** Search for a bit pattern in a row. 136 + 137 + @param bits The bitbuffer 138 + @param row Row to search 139 + @param start Starting bit position 140 + @param pattern Pattern bytes to search for (MSB aligned) 141 + @param pattern_bits Number of bits in the pattern 142 + @return Bit position of match, or end of row if not found *) 143 + val search : t -> row:int -> start:int -> pattern:bytes -> pattern_bits:int -> int 144 + 145 + (** {1 Row Comparison} *) 146 + 147 + (** Compare two rows for equality. 148 + 149 + @param bits The bitbuffer 150 + @param row_a First row index 151 + @param row_b Second row index 152 + @param max_bits Maximum bits to compare (0 = all) 153 + @return 0 if equal, non-zero otherwise *) 154 + val compare_rows : t -> row_a:int -> row_b:int -> max_bits:int -> int 155 + 156 + (** Count how many times a row is repeated. 157 + 158 + @param bits The bitbuffer 159 + @param row Row to check 160 + @param max_bits Maximum bits to compare 161 + @return Number of identical rows (at least 1) *) 162 + val count_repeats : t -> row:int -> max_bits:int -> int 163 + 164 + (** Find a row that appears at least min_repeats times. 165 + 166 + @param bits The bitbuffer 167 + @param min_repeats Minimum number of repetitions 168 + @param min_bits Minimum row length in bits 169 + @return Row index or None if not found *) 170 + val find_repeated_row : t -> min_repeats:int -> min_bits:int -> int option 171 + 172 + (** Find a repeated row by prefix matching. 173 + 174 + Like [find_repeated_row] but only compares up to [min_bits] bits. *) 175 + val find_repeated_prefix : t -> min_repeats:int -> min_bits:int -> int option 176 + 177 + (** {1 Parsing} *) 178 + 179 + (** Parse a hex string into a bitbuffer. 180 + 181 + The string may be prefixed with "0x" and rows can be separated 182 + by "/" or have length prefixes in braces like "{24}". 183 + 184 + @param bits Output bitbuffer (will be cleared first) 185 + @param code Hex string to parse *) 186 + val parse : t -> string -> unit 187 + 188 + (** {1 Printing} *) 189 + 190 + (** Pretty-print the bitbuffer contents. *) 191 + val pp : Format.formatter -> t -> unit 192 + 193 + (** Format a single row as a hex string with bit count. *) 194 + val row_to_string : t -> int -> string
+173
lib/rtl433_config.ml
··· 1 + (* Configuration *) 2 + 3 + type conversion = Rtl433_output.conversion = Native | SI | Customary 4 + type time_format = Rtl433_output.time_format = Default | ISO | Unix | Unix_usec | Off 5 + 6 + type input_spec = File of string | Rtl_tcp of string * int 7 + 8 + type output_spec = 9 + | Mqtt of Rtl433_output_mqtt.config 10 + | Json_stdout 11 + | Json_file of string 12 + | Log 13 + 14 + type t = { 15 + input : input_spec option; 16 + outputs : output_spec list; 17 + frequencies : int list; 18 + sample_rate : int; 19 + gain : float option; 20 + protocol_filter : Rtl433_registry.filter; 21 + flex_decoders : Rtl433_flex.spec list; 22 + conversion : conversion; 23 + time_format : time_format; 24 + report_protocol : bool; 25 + hop_interval : float option; 26 + duration : float option; 27 + verbosity : int; 28 + } 29 + 30 + let default = 31 + { 32 + input = None; 33 + outputs = [ Json_stdout ]; 34 + frequencies = [ 433920000 ]; 35 + sample_rate = 250000; 36 + gain = None; 37 + protocol_filter = Rtl433_registry.All; 38 + flex_decoders = []; 39 + conversion = Native; 40 + time_format = Default; 41 + report_protocol = false; 42 + hop_interval = None; 43 + duration = None; 44 + verbosity = 0; 45 + } 46 + 47 + let parse_frequency str = 48 + let str = String.trim str in 49 + let len = String.length str in 50 + if len = 0 then Error "Empty frequency" 51 + else 52 + let last = Char.lowercase_ascii str.[len - 1] in 53 + try 54 + match last with 55 + | 'm' -> 56 + let num = String.sub str 0 (len - 1) in 57 + Ok (int_of_float (Float.of_string num *. 1_000_000.0)) 58 + | 'k' -> 59 + let num = String.sub str 0 (len - 1) in 60 + Ok (int_of_string num * 1000) 61 + | '0' .. '9' -> Ok (int_of_string str) 62 + | _ -> Error ("Invalid frequency suffix: " ^ String.make 1 last) 63 + with _ -> Error ("Cannot parse frequency: " ^ str) 64 + 65 + let parse_line config line = 66 + let line = String.trim line in 67 + if String.length line = 0 || line.[0] = '#' then Ok config 68 + else 69 + let parts = String.split_on_char ' ' line in 70 + match parts with 71 + | "frequency" :: rest | "f" :: rest -> ( 72 + let freq_str = String.concat " " rest in 73 + match parse_frequency freq_str with 74 + | Ok freq -> Ok { config with frequencies = freq :: config.frequencies } 75 + | Error e -> Error e) 76 + | "protocol" :: rest | "R" :: rest -> ( 77 + match rest with 78 + | [ num ] -> ( 79 + match int_of_string_opt num with 80 + | Some n -> 81 + let filter = 82 + match config.protocol_filter with 83 + | Rtl433_registry.All -> 84 + if n < 0 then Rtl433_registry.Disable [ -n ] 85 + else Rtl433_registry.Enable [ n ] 86 + | Rtl433_registry.Enable ns -> 87 + if n < 0 then Rtl433_registry.Enable ns 88 + else Rtl433_registry.Enable (n :: ns) 89 + | Rtl433_registry.Disable ns -> 90 + if n < 0 then Rtl433_registry.Disable (-n :: ns) 91 + else Rtl433_registry.Disable ns 92 + in 93 + Ok { config with protocol_filter = filter } 94 + | None -> Error ("Invalid protocol number: " ^ List.hd rest)) 95 + | _ -> Error "protocol requires a number") 96 + | "output" :: rest -> ( 97 + let output_str = String.concat " " rest in 98 + if String.starts_with ~prefix:"mqtt://" output_str then 99 + match Rtl433_output_mqtt.parse_url output_str with 100 + | Ok mqtt_config -> 101 + Ok { config with outputs = Mqtt mqtt_config :: config.outputs } 102 + | Error e -> Error e 103 + else if output_str = "json" then 104 + Ok { config with outputs = Json_stdout :: config.outputs } 105 + else Error ("Unknown output: " ^ output_str)) 106 + | [ "convert"; "si" ] -> Ok { config with conversion = SI } 107 + | [ "convert"; "customary" ] -> Ok { config with conversion = Customary } 108 + | [ "convert"; "native" ] -> Ok { config with conversion = Native } 109 + | "report_meta" :: _ -> 110 + (* Parse report_meta time:iso:usec:tz etc *) 111 + Ok { config with time_format = ISO } 112 + | _ -> Ok config (* Ignore unknown options *) 113 + 114 + let parse_string content = 115 + let lines = String.split_on_char '\n' content in 116 + List.fold_left 117 + (fun config_result line -> 118 + match config_result with 119 + | Error e -> Error e 120 + | Ok config -> parse_line config line) 121 + (Ok default) lines 122 + 123 + let parse_file ~fs path = 124 + try 125 + let content = Eio.Path.load Eio.Path.(fs / path) in 126 + parse_string content 127 + with exn -> Error (Printexc.to_string exn) 128 + 129 + let cmdliner_term = 130 + let open Cmdliner in 131 + Term.const default 132 + 133 + let merge base overlay = 134 + { 135 + input = (match overlay.input with Some _ -> overlay.input | None -> base.input); 136 + outputs = 137 + (if overlay.outputs = [ Json_stdout ] then base.outputs else overlay.outputs); 138 + frequencies = 139 + (if overlay.frequencies = [ 433920000 ] then base.frequencies 140 + else overlay.frequencies); 141 + sample_rate = 142 + (if overlay.sample_rate = 250000 then base.sample_rate 143 + else overlay.sample_rate); 144 + gain = (match overlay.gain with Some _ -> overlay.gain | None -> base.gain); 145 + protocol_filter = 146 + (match overlay.protocol_filter with 147 + | Rtl433_registry.All -> base.protocol_filter 148 + | f -> f); 149 + flex_decoders = base.flex_decoders @ overlay.flex_decoders; 150 + conversion = 151 + (if overlay.conversion = Native then base.conversion else overlay.conversion); 152 + time_format = 153 + (if overlay.time_format = Default then base.time_format 154 + else overlay.time_format); 155 + report_protocol = overlay.report_protocol || base.report_protocol; 156 + hop_interval = 157 + (match overlay.hop_interval with 158 + | Some _ -> overlay.hop_interval 159 + | None -> base.hop_interval); 160 + duration = 161 + (match overlay.duration with Some _ -> overlay.duration | None -> base.duration); 162 + verbosity = max base.verbosity overlay.verbosity; 163 + } 164 + 165 + let pp fmt config = 166 + Format.fprintf fmt "Config {@."; 167 + Format.fprintf fmt " frequencies: [%s]@." 168 + (String.concat ", " (List.map string_of_int config.frequencies)); 169 + Format.fprintf fmt " sample_rate: %d@." config.sample_rate; 170 + Format.fprintf fmt " outputs: %d@." (List.length config.outputs); 171 + Format.fprintf fmt "}@." 172 + 173 + let to_string config = Format.asprintf "%a" pp config
+145
lib/rtl433_config.mli
··· 1 + (** Configuration for rtl433. 2 + 3 + This module handles configuration from files and command line, 4 + with syntax compatible with rtl_433. *) 5 + 6 + (** {1 Types} *) 7 + 8 + (** Unit conversion mode. *) 9 + type conversion = Rtl433_output.conversion = 10 + | Native 11 + | SI 12 + | Customary 13 + 14 + (** Time reporting format. *) 15 + type time_format = Rtl433_output.time_format = 16 + | Default 17 + | ISO 18 + | Unix 19 + | Unix_usec 20 + | Off 21 + 22 + (** Input source specification. *) 23 + type input_spec = 24 + | File of string 25 + (** File path for recorded samples. *) 26 + | Rtl_tcp of string * int 27 + (** Host and port for rtl_tcp server. *) 28 + 29 + (** Output specification. *) 30 + type output_spec = 31 + | Mqtt of Rtl433_output_mqtt.config 32 + (** MQTT output. *) 33 + | Json_stdout 34 + (** JSON to stdout. *) 35 + | Json_file of string 36 + (** JSON to file. *) 37 + | Log 38 + (** Human-readable log output. *) 39 + 40 + (** Complete configuration. *) 41 + type t = { 42 + input : input_spec option; 43 + (** Input source. *) 44 + outputs : output_spec list; 45 + (** Output handlers. *) 46 + frequencies : int list; 47 + (** Center frequencies in Hz. *) 48 + sample_rate : int; 49 + (** Sample rate in Hz. *) 50 + gain : float option; 51 + (** Gain in dB, None for auto. *) 52 + protocol_filter : Rtl433_registry.filter; 53 + (** Protocol enable/disable filter. *) 54 + flex_decoders : Rtl433_flex.spec list; 55 + (** User-defined flex decoders. *) 56 + conversion : conversion; 57 + (** Unit conversion mode. *) 58 + time_format : time_format; 59 + (** Time format for output. *) 60 + report_protocol : bool; 61 + (** Include protocol number in output. *) 62 + hop_interval : float option; 63 + (** Frequency hop interval in seconds. *) 64 + duration : float option; 65 + (** Run duration in seconds, None for unlimited. *) 66 + verbosity : int; 67 + (** Verbosity level (0=normal, 1=verbose, 2+=debug). *) 68 + } 69 + 70 + (** Default configuration. *) 71 + val default : t 72 + 73 + (** {1 Parsing} *) 74 + 75 + (** Parse a configuration file. 76 + 77 + Format is compatible with rtl_433.conf: 78 + {v 79 + output mqtt://host:port,user=x,pass=y,retain=true 80 + frequency 868M 81 + protocol 142 82 + protocol -59 83 + convert si 84 + v} 85 + 86 + @param fs Filesystem capability 87 + @param path Configuration file path 88 + @return Parsed configuration or error *) 89 + val parse_file : 90 + fs:_ Eio.Path.t -> 91 + string -> 92 + (t, string) result 93 + 94 + (** Parse configuration from string. 95 + 96 + @param content Configuration file content 97 + @return Parsed configuration or error *) 98 + val parse_string : string -> (t, string) result 99 + 100 + (** Parse a single configuration line. 101 + 102 + @param config Current configuration 103 + @param line Line to parse 104 + @return Updated configuration or error *) 105 + val parse_line : t -> string -> (t, string) result 106 + 107 + (** {1 Command Line} *) 108 + 109 + (** Cmdliner term for configuration. 110 + 111 + @return Term that produces configuration *) 112 + val cmdliner_term : t Cmdliner.Term.t 113 + 114 + (** {1 Frequency Parsing} *) 115 + 116 + (** Parse frequency string. 117 + 118 + Accepts formats like: 119 + - "433920000" (Hz) 120 + - "433.92M" (MHz) 121 + - "868M" (MHz) 122 + - "915000k" (kHz) 123 + 124 + @param str Frequency string 125 + @return Frequency in Hz or error *) 126 + val parse_frequency : string -> (int, string) result 127 + 128 + (** {1 Merging} *) 129 + 130 + (** Merge two configurations. 131 + 132 + The second configuration's non-default values override the first. 133 + 134 + @param base Base configuration 135 + @param overlay Overlay configuration 136 + @return Merged configuration *) 137 + val merge : t -> t -> t 138 + 139 + (** {1 Printing} *) 140 + 141 + (** Pretty-print configuration. *) 142 + val pp : Format.formatter -> t -> unit 143 + 144 + (** Convert configuration to config file format. *) 145 + val to_string : t -> string
+109
lib/rtl433_data.ml
··· 1 + (* Hierarchical data structure *) 2 + 3 + type value = 4 + | Int of int 5 + | Float of float 6 + | String of string 7 + | Data of t 8 + | Array of value array 9 + 10 + and field = { 11 + key : string; 12 + pretty_key : string option; 13 + format : string option; 14 + value : value; 15 + } 16 + 17 + and t = field list 18 + 19 + let make items = 20 + List.map 21 + (fun (key, pretty_key, value) -> { key; pretty_key; format = None; value }) 22 + items 23 + 24 + let int key ?pretty ?format value = 25 + { key; pretty_key = pretty; format; value = Int value } 26 + 27 + let float key ?pretty ?format value = 28 + { key; pretty_key = pretty; format; value = Float value } 29 + 30 + let string key ?pretty ?format value = 31 + { key; pretty_key = pretty; format; value = String value } 32 + 33 + let data key ?pretty value = 34 + { key; pretty_key = pretty; format = None; value = Data value } 35 + 36 + let array key ?pretty value = 37 + { key; pretty_key = pretty; format = None; value = Array value } 38 + 39 + let hex key ?pretty bytes len = 40 + let s = Rtl433_util.bytes_to_hex bytes len in 41 + { key; pretty_key = pretty; format = None; value = String s } 42 + 43 + let find key data = List.find_opt (fun f -> f.key = key) data 44 + 45 + let get_int key data = 46 + match find key data with Some { value = Int i; _ } -> Some i | _ -> None 47 + 48 + let get_float key data = 49 + match find key data with Some { value = Float f; _ } -> Some f | _ -> None 50 + 51 + let get_string key data = 52 + match find key data with Some { value = String s; _ } -> Some s | _ -> None 53 + 54 + let rec pp_value fmt = function 55 + | Int i -> Format.fprintf fmt "%d" i 56 + | Float f -> Format.fprintf fmt "%g" f 57 + | String s -> Format.fprintf fmt "%S" s 58 + | Data d -> pp fmt d 59 + | Array a -> 60 + Format.fprintf fmt "[%a]" 61 + (Format.pp_print_list ~pp_sep:(fun fmt () -> Format.fprintf fmt ", ") pp_value) 62 + (Array.to_list a) 63 + 64 + and pp fmt data = 65 + Format.fprintf fmt "{%a}" 66 + (Format.pp_print_list 67 + ~pp_sep:(fun fmt () -> Format.fprintf fmt ", ") 68 + (fun fmt f -> Format.fprintf fmt "%s: %a" f.key pp_value f.value)) 69 + data 70 + 71 + let to_json data = 72 + let buf = Buffer.create 256 in 73 + let rec emit_value = function 74 + | Int i -> Buffer.add_string buf (string_of_int i) 75 + | Float f -> Buffer.add_string buf (string_of_float f) 76 + | String s -> 77 + Buffer.add_char buf '"'; 78 + Buffer.add_string buf (String.escaped s); 79 + Buffer.add_char buf '"' 80 + | Data d -> emit_data d 81 + | Array a -> 82 + Buffer.add_char buf '['; 83 + Array.iteri 84 + (fun i v -> 85 + if i > 0 then Buffer.add_char buf ','; 86 + emit_value v) 87 + a; 88 + Buffer.add_char buf ']' 89 + and emit_data fields = 90 + Buffer.add_char buf '{'; 91 + List.iteri 92 + (fun i f -> 93 + if i > 0 then Buffer.add_char buf ','; 94 + Buffer.add_char buf '"'; 95 + Buffer.add_string buf f.key; 96 + Buffer.add_string buf "\":"; 97 + emit_value f.value) 98 + fields; 99 + Buffer.add_char buf '}' 100 + in 101 + emit_data data; 102 + Buffer.contents buf 103 + 104 + (* Mutable storage for last decoded output *) 105 + let last_output : t option ref = ref None 106 + 107 + let set_last_output data = last_output := Some data 108 + let get_last_output () = !last_output 109 + let clear_last_output () = last_output := None
+125
lib/rtl433_data.mli
··· 1 + (** Hierarchical data structure for decoded sensor messages. 2 + 3 + This module provides a flexible data structure for representing 4 + decoded sensor data with typed fields. The structure supports 5 + nested data, arrays, and various scalar types, and can be 6 + serialized to JSON or other formats. *) 7 + 8 + (** {1 Data Values} *) 9 + 10 + (** A data value that can be stored in a field. *) 11 + type value = 12 + | Int of int (** Integer value *) 13 + | Float of float (** Floating-point value *) 14 + | String of string (** String value *) 15 + | Data of t (** Nested data structure *) 16 + | Array of value array (** Array of homogeneous values *) 17 + 18 + (** A single field in the data structure. *) 19 + and field = { 20 + key : string; (** Field identifier (e.g., "temperature_C") *) 21 + pretty_key : string option;(** Human-readable name (e.g., "Temperature") *) 22 + format : string option; (** Printf-style format string *) 23 + value : value; (** The field's value *) 24 + } 25 + 26 + (** A data structure is a list of fields. *) 27 + and t = field list 28 + 29 + (** {1 Construction} *) 30 + 31 + (** Create a data structure from a list of (key, pretty_key, value) tuples. *) 32 + val make : (string * string option * value) list -> t 33 + 34 + (** Create an integer field. 35 + 36 + @param key Field key 37 + @param pretty Optional pretty name 38 + @param format Optional format string (e.g., "%d") 39 + @param value Integer value *) 40 + val int : string -> ?pretty:string -> ?format:string -> int -> field 41 + 42 + (** Create a float field. 43 + 44 + @param key Field key 45 + @param pretty Optional pretty name 46 + @param format Optional format string (e.g., "%.1f C") 47 + @param value Float value *) 48 + val float : string -> ?pretty:string -> ?format:string -> float -> field 49 + 50 + (** Create a string field. 51 + 52 + @param key Field key 53 + @param pretty Optional pretty name 54 + @param format Optional format string 55 + @param value String value *) 56 + val string : string -> ?pretty:string -> ?format:string -> string -> field 57 + 58 + (** Create a nested data field. 59 + 60 + @param key Field key 61 + @param pretty Optional pretty name 62 + @param value Nested data structure *) 63 + val data : string -> ?pretty:string -> t -> field 64 + 65 + (** Create an array field. 66 + 67 + @param key Field key 68 + @param pretty Optional pretty name 69 + @param value Array of values *) 70 + val array : string -> ?pretty:string -> value array -> field 71 + 72 + (** Create a hex string field from bytes. 73 + 74 + @param key Field key 75 + @param pretty Optional pretty name 76 + @param bytes Raw bytes 77 + @param len Number of bytes to include *) 78 + val hex : string -> ?pretty:string -> bytes -> int -> field 79 + 80 + (** {1 Field Access} *) 81 + 82 + (** Find a field by key. 83 + 84 + @param key Field key to search for 85 + @param data Data structure to search 86 + @return Field if found *) 87 + val find : string -> t -> field option 88 + 89 + (** Get an integer value by key. 90 + 91 + @param key Field key 92 + @param data Data structure 93 + @return Integer value if field exists and is an Int *) 94 + val get_int : string -> t -> int option 95 + 96 + (** Get a float value by key. *) 97 + val get_float : string -> t -> float option 98 + 99 + (** Get a string value by key. *) 100 + val get_string : string -> t -> string option 101 + 102 + (** {1 Serialization} *) 103 + 104 + (** Convert data structure to JSON string. 105 + 106 + @param data Data structure 107 + @return JSON representation *) 108 + val to_json : t -> string 109 + 110 + (** Pretty-print data structure. *) 111 + val pp : Format.formatter -> t -> unit 112 + 113 + (** Pretty-print a single value. *) 114 + val pp_value : Format.formatter -> value -> unit 115 + 116 + (** {1 Decoder Output} *) 117 + 118 + (** Set the last decoded output (called by decoders). *) 119 + val set_last_output : t -> unit 120 + 121 + (** Get the last decoded output (called by pipeline). *) 122 + val get_last_output : unit -> t option 123 + 124 + (** Clear the last decoded output. *) 125 + val clear_last_output : unit -> unit
+174
lib/rtl433_decoder_fineoffset.ml
··· 1 + (* FineOffset WH51 Soil Moisture Sensor decoder 2 + 3 + Protocol 142 in rtl_433 4 + 5 + Modulation: FSK_PULSE_PCM 6 + Bit width: 58 µs 7 + 8 + Packet format: 9 + - Preamble: 0xAA 0x2D 0xD4 10 + - 14 bytes data 11 + 12 + Data layout: 13 + - byte 0: Family code 0x51 14 + - byte 1-3: Device ID (24 bits) 15 + - byte 4 high nibble: Battery status (0=OK, 8=low) 16 + - byte 4 low nibble: Transmission counter 17 + - byte 5: Moisture percentage (0-100) 18 + - byte 6-7: Raw AD value (big endian) 19 + - byte 8: Boost/interval 20 + - byte 9-10: Battery voltage (raw) 21 + - byte 11: Sequence/flags 22 + - byte 12: CRC8 (poly 0x31, init 0x00) over bytes 0-11 23 + - byte 13: Checksum (sum of bytes 0-12 & 0xff) 24 + *) 25 + 26 + (* Find the preamble 0xAA 0x2D 0xD4 in the bit buffer *) 27 + let find_preamble bits = 28 + let num_bits = Rtl433_bitbuffer.bits_per_row bits 0 in 29 + let preamble = [| 0xAA; 0x2D; 0xD4 |] in 30 + 31 + let rec search bit_offset = 32 + if bit_offset + (14 * 8) > num_bits then None 33 + else begin 34 + let matches = ref true in 35 + for i = 0 to 2 do 36 + if !matches then begin 37 + let byte = Rtl433_bitbuffer.get_byte bits ~row:0 ~bit_offset:(bit_offset + (i * 8)) in 38 + if byte <> preamble.(i) then matches := false 39 + end 40 + done; 41 + if !matches then Some (bit_offset + 24) (* Skip past preamble *) 42 + else search (bit_offset + 1) 43 + end 44 + in 45 + search 0 46 + 47 + let decode_wh51 () bits = 48 + let num_bits = Rtl433_bitbuffer.bits_per_row bits 0 in 49 + 50 + (* Need at least preamble (24 bits) + 14 bytes data *) 51 + if num_bits < 24 + (14 * 8) then 52 + Rtl433_device.Abort_length 53 + else 54 + match find_preamble bits with 55 + | None -> Rtl433_device.Abort_early 56 + | Some data_start -> 57 + (* Extract 14 bytes of data *) 58 + let data = Bytes.create 14 in 59 + for i = 0 to 13 do 60 + let byte = Rtl433_bitbuffer.get_byte bits ~row:0 ~bit_offset:(data_start + (i * 8)) in 61 + Bytes.set_uint8 data i byte 62 + done; 63 + 64 + (* Check family code *) 65 + let family = Bytes.get_uint8 data 0 in 66 + if family <> 0x51 then 67 + Rtl433_device.Fail_sanity 68 + else begin 69 + (* Verify CRC8 over bytes 0-11 *) 70 + let crc = Rtl433_util.crc8 ~poly:0x31 ~init:0x00 data 12 in 71 + let expected_crc = Bytes.get_uint8 data 12 in 72 + if crc <> expected_crc then 73 + Rtl433_device.Fail_mic 74 + else begin 75 + (* Verify checksum *) 76 + let sum = Rtl433_util.add_bytes data 13 in 77 + let expected_sum = Bytes.get_uint8 data 13 in 78 + if sum <> expected_sum then 79 + Rtl433_device.Fail_mic 80 + else begin 81 + (* Extract fields *) 82 + let id = 83 + (Bytes.get_uint8 data 1 lsl 16) lor 84 + (Bytes.get_uint8 data 2 lsl 8) lor 85 + (Bytes.get_uint8 data 3) 86 + in 87 + let id_str = Printf.sprintf "%06x" id in 88 + 89 + let battery_status = (Bytes.get_uint8 data 4 lsr 4) land 0x0f in 90 + let battery_ok = if battery_status = 0 then 1.0 else 0.0 in 91 + 92 + let counter = Bytes.get_uint8 data 4 land 0x0f in 93 + let moisture = Bytes.get_uint8 data 5 in 94 + 95 + let ad_raw = 96 + (Bytes.get_uint8 data 6 lsl 8) lor 97 + (Bytes.get_uint8 data 7) 98 + in 99 + 100 + let boost = Bytes.get_uint8 data 8 in 101 + 102 + let battery_raw = 103 + (Bytes.get_uint8 data 9 lsl 8) lor 104 + (Bytes.get_uint8 data 10) 105 + in 106 + (* Battery voltage: raw * 2 mV according to analysis *) 107 + let battery_mv = battery_raw * 2 in 108 + 109 + (* Output data *) 110 + let output_data = Rtl433_data.make [ 111 + ("model", None, Rtl433_data.String "Fineoffset-WH51"); 112 + ("id", None, Rtl433_data.String id_str); 113 + ("battery_ok", None, Rtl433_data.Float battery_ok); 114 + ("battery_mV", Some "mV", Rtl433_data.Int battery_mv); 115 + ("moisture", Some "%", Rtl433_data.Int moisture); 116 + ("boost", None, Rtl433_data.Int boost); 117 + ("ad_raw", None, Rtl433_data.Int ad_raw); 118 + ("counter", None, Rtl433_data.Int counter); 119 + ("mic", None, Rtl433_data.String "CRC"); 120 + ] in 121 + 122 + (* Store output data for later retrieval *) 123 + Rtl433_data.set_last_output output_data; 124 + 125 + Rtl433_device.Decoded 1 126 + end 127 + end 128 + end 129 + 130 + let create_wh51 () = 131 + let timing = Rtl433_device.make_timing 132 + ~short_width:58.0 (* 58 µs bit width *) 133 + ~long_width:58.0 134 + ~reset_limit:10000.0 (* 10 ms reset *) 135 + ~tolerance:10.0 136 + () 137 + in 138 + Rtl433_device.create 139 + ~name:"Fineoffset-WH51" 140 + ~protocol_num:142 141 + ~modulation:(Rtl433_modulation.FSK Rtl433_modulation.Pulse_pcm) 142 + ~timing 143 + ~decode:decode_wh51 144 + ~fields:["model"; "id"; "battery_ok"; "battery_mV"; "moisture"; "boost"; "ad_raw"; "counter"; "mic"] 145 + () 146 + 147 + (* Additional FineOffset decoders can be added here *) 148 + 149 + (* WH2 Temperature/Humidity sensor - protocol 32 *) 150 + let decode_wh2 () _bits = 151 + (* TODO: implement *) 152 + Rtl433_device.Abort_early 153 + 154 + let create_wh2 () = 155 + let timing = Rtl433_device.make_timing 156 + ~short_width:500.0 157 + ~long_width:1500.0 158 + ~reset_limit:9000.0 159 + () 160 + in 161 + Rtl433_device.create 162 + ~name:"Fineoffset-WH2" 163 + ~protocol_num:32 164 + ~modulation:(Rtl433_modulation.OOK Rtl433_modulation.Pulse_pwm) 165 + ~timing 166 + ~decode:decode_wh2 167 + ~fields:["model"; "id"; "temperature_C"; "humidity"] 168 + () 169 + 170 + (* Return all FineOffset decoders *) 171 + let all_decoders () = [ 172 + create_wh51 (); 173 + create_wh2 (); 174 + ]
+121
lib/rtl433_device.ml
··· 1 + (* Protocol decoder interface *) 2 + 3 + type decode_result = 4 + | Decoded of int 5 + | Abort_length 6 + | Abort_early 7 + | Fail_mic 8 + | Fail_sanity 9 + 10 + type timing = { 11 + short_width : float; 12 + long_width : float; 13 + reset_limit : float; 14 + gap_limit : float option; 15 + sync_width : float option; 16 + tolerance : float; 17 + } 18 + 19 + type stats = { 20 + mutable decode_events : int; 21 + mutable decode_ok : int; 22 + mutable decode_messages : int; 23 + mutable decode_fails : int array; 24 + } 25 + 26 + let create_stats () = 27 + { 28 + decode_events = 0; 29 + decode_ok = 0; 30 + decode_messages = 0; 31 + decode_fails = Array.make 5 0; 32 + } 33 + 34 + type 'ctx t = { 35 + protocol_num : int; 36 + name : string; 37 + modulation : Rtl433_modulation.t; 38 + timing : timing; 39 + decode : 'ctx -> Rtl433_bitbuffer.t -> decode_result; 40 + output : Rtl433_data.t -> unit; 41 + priority : int; 42 + disabled : bool; 43 + fields : string list; 44 + stats : stats; 45 + ctx : 'ctx; 46 + } 47 + 48 + let create ~name ~protocol_num ~modulation ~timing ~decode ~fields 49 + ?(priority = 0) ?(disabled = false) ctx = 50 + { 51 + protocol_num; 52 + name; 53 + modulation; 54 + timing; 55 + decode; 56 + output = (fun _ -> ()); 57 + priority; 58 + disabled; 59 + fields; 60 + stats = create_stats (); 61 + ctx; 62 + } 63 + 64 + let set_output t output = { t with output } 65 + 66 + let make_timing ~short_width ~long_width ~reset_limit ?gap_limit ?sync_width 67 + ?tolerance () = 68 + let tolerance = 69 + match tolerance with Some t -> t | None -> short_width *. 0.1 70 + in 71 + { short_width; long_width; reset_limit; gap_limit; sync_width; tolerance } 72 + 73 + let run t pulse_data = 74 + t.stats.decode_events <- t.stats.decode_events + 1; 75 + (* Slice the pulse data into bits *) 76 + let bits = Rtl433_pulse_slicer.slice pulse_data t.modulation 77 + ~short_width:t.timing.short_width 78 + ~long_width:t.timing.long_width 79 + ~tolerance:t.timing.tolerance 80 + ?sync_width:t.timing.sync_width 81 + () 82 + in 83 + let result = t.decode t.ctx bits in 84 + (match result with 85 + | Decoded n -> 86 + t.stats.decode_ok <- t.stats.decode_ok + 1; 87 + t.stats.decode_messages <- t.stats.decode_messages + n 88 + | Abort_length -> t.stats.decode_fails.(0) <- t.stats.decode_fails.(0) + 1 89 + | Abort_early -> t.stats.decode_fails.(1) <- t.stats.decode_fails.(1) + 1 90 + | Fail_mic -> t.stats.decode_fails.(2) <- t.stats.decode_fails.(2) + 1 91 + | Fail_sanity -> t.stats.decode_fails.(3) <- t.stats.decode_fails.(3) + 1); 92 + result 93 + 94 + let run_on_bits t bits = 95 + t.stats.decode_events <- t.stats.decode_events + 1; 96 + let result = t.decode t.ctx bits in 97 + (match result with 98 + | Decoded n -> 99 + t.stats.decode_ok <- t.stats.decode_ok + 1; 100 + t.stats.decode_messages <- t.stats.decode_messages + n 101 + | Abort_length -> t.stats.decode_fails.(0) <- t.stats.decode_fails.(0) + 1 102 + | Abort_early -> t.stats.decode_fails.(1) <- t.stats.decode_fails.(1) + 1 103 + | Fail_mic -> t.stats.decode_fails.(2) <- t.stats.decode_fails.(2) + 1 104 + | Fail_sanity -> t.stats.decode_fails.(3) <- t.stats.decode_fails.(3) + 1); 105 + result 106 + 107 + let pp fmt t = 108 + Format.fprintf fmt "[%d] %s (%a)" t.protocol_num t.name Rtl433_modulation.pp 109 + t.modulation 110 + 111 + let pp_result fmt = function 112 + | Decoded n -> Format.fprintf fmt "Decoded %d" n 113 + | Abort_length -> Format.fprintf fmt "Abort (length)" 114 + | Abort_early -> Format.fprintf fmt "Abort (early)" 115 + | Fail_mic -> Format.fprintf fmt "Fail (MIC)" 116 + | Fail_sanity -> Format.fprintf fmt "Fail (sanity)" 117 + 118 + let pp_stats fmt s = 119 + Format.fprintf fmt "events=%d ok=%d msgs=%d fails=[%d,%d,%d,%d,%d]" 120 + s.decode_events s.decode_ok s.decode_messages s.decode_fails.(0) 121 + s.decode_fails.(1) s.decode_fails.(2) s.decode_fails.(3) s.decode_fails.(4)
+166
lib/rtl433_device.mli
··· 1 + (** Protocol decoder interface. 2 + 3 + This module defines the interface that all protocol decoders must 4 + implement. Each decoder handles a specific wireless protocol and 5 + converts bitbuffer data into structured sensor readings. *) 6 + 7 + (** {1 Decode Results} *) 8 + 9 + (** Result of attempting to decode a bitbuffer. *) 10 + type decode_result = 11 + | Decoded of int 12 + (** Successfully decoded n messages. *) 13 + | Abort_length 14 + (** Wrong row count or row length for this protocol. *) 15 + | Abort_early 16 + (** Early abort - data doesn't match expected pattern. *) 17 + | Fail_mic 18 + (** Message Integrity Check failed (CRC/checksum error). *) 19 + | Fail_sanity 20 + (** Sanity check failed (values out of expected range). *) 21 + 22 + (** {1 Timing Parameters} *) 23 + 24 + (** Timing parameters for pulse detection and slicing. *) 25 + type timing = { 26 + short_width : float; 27 + (** Nominal short pulse/gap width in microseconds. *) 28 + long_width : float; 29 + (** Nominal long pulse/gap width in microseconds. *) 30 + reset_limit : float; 31 + (** Maximum gap before declaring end of package (us). *) 32 + gap_limit : float option; 33 + (** Maximum gap within a package (us). None for unlimited. *) 34 + sync_width : float option; 35 + (** Sync pulse width in microseconds, if used. *) 36 + tolerance : float; 37 + (** Timing tolerance in microseconds. *) 38 + } 39 + 40 + (** {1 Decoder Statistics} *) 41 + 42 + (** Runtime statistics for a decoder. *) 43 + type stats = { 44 + mutable decode_events : int; 45 + (** Total decode attempts. *) 46 + mutable decode_ok : int; 47 + (** Successful decodes. *) 48 + mutable decode_messages : int; 49 + (** Total messages decoded. *) 50 + mutable decode_fails : int array; 51 + (** Failure counts by type (length, early, mic, sanity, other). *) 52 + } 53 + 54 + (** Create fresh statistics. *) 55 + val create_stats : unit -> stats 56 + 57 + (** {1 Decoder Definition} *) 58 + 59 + (** A protocol decoder. 60 + 61 + The type parameter ['ctx] allows decoders to carry custom state 62 + or configuration. For stateless decoders, use [unit]. *) 63 + type 'ctx t = { 64 + protocol_num : int; 65 + (** Protocol number for filtering (matches rtl_433 numbering). *) 66 + name : string; 67 + (** Human-readable protocol name. *) 68 + modulation : Rtl433_modulation.t; 69 + (** Expected RF modulation type. *) 70 + timing : timing; 71 + (** Pulse timing parameters. *) 72 + decode : 'ctx -> Rtl433_bitbuffer.t -> decode_result; 73 + (** Decode function: takes context and bitbuffer, returns result. *) 74 + output : Rtl433_data.t -> unit; 75 + (** Output callback for decoded data. *) 76 + priority : int; 77 + (** Priority level (higher = run later, only if no prior events). *) 78 + disabled : bool; 79 + (** Whether decoder is disabled by default. *) 80 + fields : string list; 81 + (** List of output field names (for CSV header). *) 82 + stats : stats; 83 + (** Runtime statistics. *) 84 + ctx : 'ctx; 85 + (** Decoder-specific context or configuration. *) 86 + } 87 + 88 + (** {1 Construction} *) 89 + 90 + (** Create a new decoder. 91 + 92 + @param name Protocol name 93 + @param protocol_num Protocol number 94 + @param modulation Modulation type 95 + @param timing Timing parameters 96 + @param decode Decode function 97 + @param fields Output field names 98 + @param priority Priority level (default 0) 99 + @param disabled Default disabled state (default false) 100 + @param ctx Decoder context 101 + @return New decoder instance *) 102 + val create : 103 + name:string -> 104 + protocol_num:int -> 105 + modulation:Rtl433_modulation.t -> 106 + timing:timing -> 107 + decode:('ctx -> Rtl433_bitbuffer.t -> decode_result) -> 108 + fields:string list -> 109 + ?priority:int -> 110 + ?disabled:bool -> 111 + 'ctx -> 112 + 'ctx t 113 + 114 + (** Set the output callback for a decoder. *) 115 + val set_output : 'ctx t -> (Rtl433_data.t -> unit) -> 'ctx t 116 + 117 + (** {1 Timing Helpers} *) 118 + 119 + (** Create timing parameters with common defaults. 120 + 121 + @param short_width Short pulse width in microseconds 122 + @param long_width Long pulse width in microseconds 123 + @param reset_limit Reset limit in microseconds 124 + @param gap_limit Optional gap limit 125 + @param sync_width Optional sync width 126 + @param tolerance Tolerance (default 10% of short_width) 127 + @return Timing parameters *) 128 + val make_timing : 129 + short_width:float -> 130 + long_width:float -> 131 + reset_limit:float -> 132 + ?gap_limit:float -> 133 + ?sync_width:float -> 134 + ?tolerance:float -> 135 + unit -> 136 + timing 137 + 138 + (** {1 Running Decoders} *) 139 + 140 + (** Run a decoder on pulse data. 141 + 142 + This handles the full pipeline: slicing pulses to bits, 143 + then running the decoder, updating statistics. 144 + 145 + @param decoder The decoder to run 146 + @param pulse_data Pulse timing data 147 + @return Decode result *) 148 + val run : 'ctx t -> Rtl433_pulse_data.t -> decode_result 149 + 150 + (** Run decoder directly on a bitbuffer. 151 + 152 + @param decoder The decoder to run 153 + @param bits Bitbuffer to decode 154 + @return Decode result *) 155 + val run_on_bits : 'ctx t -> Rtl433_bitbuffer.t -> decode_result 156 + 157 + (** {1 Printing} *) 158 + 159 + (** Pretty-print decoder info. *) 160 + val pp : Format.formatter -> 'ctx t -> unit 161 + 162 + (** Pretty-print decode result. *) 163 + val pp_result : Format.formatter -> decode_result -> unit 164 + 165 + (** Pretty-print statistics. *) 166 + val pp_stats : Format.formatter -> stats -> unit
+70
lib/rtl433_flex.ml
··· 1 + (* Flexible decoder system *) 2 + 3 + type encoding = PCM | PWM | PPM | Manchester | Differential_manchester 4 + 5 + type spec = { 6 + name : string; 7 + modulation : Rtl433_modulation.t; 8 + short_width : float; 9 + long_width : float; 10 + reset_limit : float; 11 + gap_limit : float option; 12 + sync_width : float option; 13 + tolerance : float; 14 + preamble : bytes option; 15 + encoding : encoding; 16 + min_bits : int; 17 + max_bits : int option; 18 + match_len : int option; 19 + count_only : bool; 20 + invert : bool; 21 + reflect_bytes : bool; 22 + unique : bool; 23 + fields : string list; 24 + } 25 + 26 + let default_spec = 27 + { 28 + name = "Flex"; 29 + modulation = Rtl433_modulation.OOK Rtl433_modulation.Pulse_pwm; 30 + short_width = 500.0; 31 + long_width = 1000.0; 32 + reset_limit = 4000.0; 33 + gap_limit = None; 34 + sync_width = None; 35 + tolerance = 100.0; 36 + preamble = None; 37 + encoding = PWM; 38 + min_bits = 8; 39 + max_bits = None; 40 + match_len = None; 41 + count_only = false; 42 + invert = false; 43 + reflect_bytes = false; 44 + unique = false; 45 + fields = [ "model"; "count"; "data" ]; 46 + } 47 + 48 + let create spec = 49 + let timing = 50 + Rtl433_device.make_timing ~short_width:spec.short_width 51 + ~long_width:spec.long_width ~reset_limit:spec.reset_limit 52 + ?gap_limit:spec.gap_limit ?sync_width:spec.sync_width 53 + ~tolerance:spec.tolerance () 54 + in 55 + let decode () bits = 56 + let num_bits = Rtl433_bitbuffer.bits_per_row bits 0 in 57 + if num_bits < spec.min_bits then Rtl433_device.Abort_length 58 + else Rtl433_device.Decoded 1 59 + in 60 + Rtl433_device.create ~name:spec.name ~protocol_num:0 ~modulation:spec.modulation 61 + ~timing ~decode ~fields:spec.fields () 62 + 63 + let parse _str = Error "Not implemented" 64 + 65 + let pp_spec fmt spec = 66 + Format.fprintf fmt "Flex<%s> m=%a s=%.0f l=%.0f r=%.0f" spec.name 67 + Rtl433_modulation.pp spec.modulation spec.short_width spec.long_width 68 + spec.reset_limit 69 + 70 + let spec_to_string spec = Format.asprintf "%a" pp_spec spec
+104
lib/rtl433_flex.mli
··· 1 + (** Flexible decoder system for user-defined protocols. 2 + 3 + The flex decoder allows defining custom protocol decoders via 4 + configuration rather than code. This enables adding support for 5 + new sensors without modifying the library. *) 6 + 7 + (** {1 Flex Decoder Specification} *) 8 + 9 + (** Bit encoding type for the flex decoder. *) 10 + type encoding = 11 + | PCM 12 + (** Pulse Code Modulation (NRZ): pulse=1, no pulse=0. *) 13 + | PWM 14 + (** Pulse Width Modulation: short=1, long=0. *) 15 + | PPM 16 + (** Pulse Position Modulation: short gap=0, long gap=1. *) 17 + | Manchester 18 + (** Manchester encoding: transition direction determines bit. *) 19 + | Differential_manchester 20 + (** Differential Manchester: transition presence determines bit. *) 21 + 22 + (** Specification for a flex decoder. *) 23 + type spec = { 24 + name : string; 25 + (** Protocol name. *) 26 + modulation : Rtl433_modulation.t; 27 + (** RF modulation type. *) 28 + short_width : float; 29 + (** Short pulse/gap width in microseconds. *) 30 + long_width : float; 31 + (** Long pulse/gap width in microseconds. *) 32 + reset_limit : float; 33 + (** End of transmission gap limit in microseconds. *) 34 + gap_limit : float option; 35 + (** Maximum gap within transmission (us). *) 36 + sync_width : float option; 37 + (** Sync pulse width in microseconds. *) 38 + tolerance : float; 39 + (** Timing tolerance in microseconds. *) 40 + preamble : bytes option; 41 + (** Expected preamble bytes (for validation). *) 42 + encoding : encoding; 43 + (** Bit encoding type. *) 44 + min_bits : int; 45 + (** Minimum valid message length in bits. *) 46 + max_bits : int option; 47 + (** Maximum valid message length in bits. *) 48 + match_len : int option; 49 + (** Expected exact message length in bits. *) 50 + count_only : bool; 51 + (** If true, just count bits without further processing. *) 52 + invert : bool; 53 + (** If true, invert all bits before processing. *) 54 + reflect_bytes : bool; 55 + (** If true, reverse bit order within each byte. *) 56 + unique : bool; 57 + (** If true, deduplicate repeated rows. *) 58 + fields : string list; 59 + (** Output field names. *) 60 + } 61 + 62 + (** {1 Construction} *) 63 + 64 + (** Create a flex decoder from a specification. 65 + 66 + @param spec Flex decoder specification 67 + @return A decoder that can be registered *) 68 + val create : spec -> unit Rtl433_device.t 69 + 70 + (** Default flex specification with common values. *) 71 + val default_spec : spec 72 + 73 + (** {1 Parsing} *) 74 + 75 + (** Parse a flex decoder specification from a string. 76 + 77 + Format matches rtl_433's -X option: 78 + {v 79 + n=name,m=OOK_PWM,s=400,l=800,r=4000,g=1000,t=100 80 + v} 81 + 82 + Keys: 83 + - n: name 84 + - m: modulation (OOK_PCM, OOK_PWM, OOK_PPM, OOK_MC, FSK_PCM, etc.) 85 + - s: short width (us) 86 + - l: long width (us) 87 + - r: reset limit (us) 88 + - g: gap limit (us) 89 + - t: tolerance (us) 90 + - preamble: hex bytes 91 + - bits: expected bit count 92 + - unique: deduplicate rows 93 + 94 + @param str Specification string 95 + @return Parsed spec, or Error message *) 96 + val parse : string -> (spec, string) result 97 + 98 + (** {1 Printing} *) 99 + 100 + (** Pretty-print a flex specification. *) 101 + val pp_spec : Format.formatter -> spec -> unit 102 + 103 + (** Convert spec to rtl_433-compatible string. *) 104 + val spec_to_string : spec -> string
+136
lib/rtl433_input.ml
··· 1 + (* Input sources *) 2 + 3 + type format = Rtl433_baseband.sample_format = CU8 | CS8 | CS16 | CF32 4 + 5 + (* Use an opaque type for the socket to avoid type variance issues *) 6 + type socket 7 + 8 + let socket_of_flow (flow : 'a) : socket = Obj.magic flow 9 + let flow_of_socket (sock : socket) : Eio.Flow.two_way_ty Eio.Resource.t = Obj.magic sock 10 + 11 + type source = 12 + | File_source of { 13 + flow : Eio.File.ro_ty Eio.Resource.t; 14 + format : format; 15 + sample_rate : int; 16 + } 17 + | Rtl_tcp_source of { 18 + flow : socket; 19 + mutable frequency : int; 20 + mutable sample_rate : int; 21 + mutable gain : int; 22 + } 23 + 24 + type t = { source : source } 25 + 26 + let open_file ~sw ~fs ~path ~format ~sample_rate = 27 + let full_path = Eio.Path.(fs / path) in 28 + let flow = Eio.Path.open_in ~sw full_path in 29 + { source = File_source { flow; format; sample_rate } } 30 + 31 + let detect_format filename = 32 + let filename = String.lowercase_ascii filename in 33 + if String.ends_with ~suffix:".cu8" filename then Some CU8 34 + else if String.ends_with ~suffix:".cs8" filename then Some CS8 35 + else if String.ends_with ~suffix:".cs16" filename then Some CS16 36 + else if String.ends_with ~suffix:".cf32" filename then Some CF32 37 + else None 38 + 39 + let detect_sample_rate filename = 40 + (* Look for patterns like 250k, 1M, 2.4M *) 41 + let re_k = Str.regexp "\\([0-9]+\\)k" in 42 + let re_m = Str.regexp "\\([0-9.]+\\)[mM]" in 43 + try 44 + if Str.search_forward re_m filename 0 >= 0 then 45 + let m = Str.matched_group 1 filename in 46 + Some (int_of_float (Float.of_string m *. 1_000_000.0)) 47 + else if Str.search_forward re_k filename 0 >= 0 then 48 + let k = Str.matched_group 1 filename in 49 + Some (int_of_string k * 1000) 50 + else None 51 + with Not_found -> None 52 + 53 + let connect_rtl_tcp ~sw ~net ~host ?(port = 1234) () = 54 + let addr = `Tcp (Eio.Net.Ipaddr.of_raw (Unix.inet_addr_of_string host |> Obj.magic), port) in 55 + let flow = Eio.Net.connect ~sw net addr in 56 + { 57 + source = 58 + Rtl_tcp_source { flow = socket_of_flow flow; frequency = 433920000; sample_rate = 250000; gain = 0 }; 59 + } 60 + 61 + let send_rtl_tcp_cmd flow cmd param = 62 + let buf = Bytes.create 5 in 63 + Bytes.set_uint8 buf 0 cmd; 64 + Bytes.set_int32_be buf 1 (Int32.of_int param); 65 + Eio.Flow.write flow [ Cstruct.of_bytes buf ] 66 + 67 + let set_frequency t freq = 68 + match t.source with 69 + | Rtl_tcp_source s -> 70 + send_rtl_tcp_cmd (flow_of_socket s.flow) 0x01 freq; 71 + s.frequency <- freq 72 + | File_source _ -> () 73 + 74 + let set_sample_rate t rate = 75 + match t.source with 76 + | Rtl_tcp_source s -> 77 + send_rtl_tcp_cmd (flow_of_socket s.flow) 0x02 rate; 78 + s.sample_rate <- rate 79 + | File_source _ -> () 80 + 81 + let set_gain t gain = 82 + match t.source with 83 + | Rtl_tcp_source s -> 84 + send_rtl_tcp_cmd (flow_of_socket s.flow) 0x04 gain; 85 + s.gain <- gain 86 + | File_source _ -> () 87 + 88 + let set_agc t enable = 89 + match t.source with 90 + | Rtl_tcp_source s -> send_rtl_tcp_cmd (flow_of_socket s.flow) 0x08 (if enable then 1 else 0) 91 + | File_source _ -> () 92 + 93 + let read t buf = 94 + match t.source with 95 + | File_source { flow; _ } -> 96 + Eio.Flow.single_read flow (Cstruct.of_bytes buf) 97 + | Rtl_tcp_source { flow; _ } -> 98 + Eio.Flow.single_read (flow_of_socket flow) (Cstruct.of_bytes buf) 99 + 100 + let read_timeout t buf ~clock ~timeout_ms = 101 + try 102 + Eio.Time.with_timeout_exn clock (timeout_ms /. 1000.0) (fun () -> read t buf) 103 + with Eio.Time.Timeout -> 0 104 + 105 + let sample_rate t = 106 + match t.source with 107 + | File_source { sample_rate; _ } -> sample_rate 108 + | Rtl_tcp_source { sample_rate; _ } -> sample_rate 109 + 110 + let format t = 111 + match t.source with 112 + | File_source { format; _ } -> format 113 + | Rtl_tcp_source _ -> CU8 (* rtl_tcp always uses CU8 *) 114 + 115 + let frequency t = 116 + match t.source with 117 + | File_source _ -> 0 118 + | Rtl_tcp_source { frequency; _ } -> frequency 119 + 120 + let is_network t = 121 + match t.source with File_source _ -> false | Rtl_tcp_source _ -> true 122 + 123 + let close _t = () (* Resources cleaned up by Eio switch *) 124 + 125 + let format_to_string = function 126 + | CU8 -> "CU8" 127 + | CS8 -> "CS8" 128 + | CS16 -> "CS16" 129 + | CF32 -> "CF32" 130 + 131 + let pp fmt t = 132 + match t.source with 133 + | File_source { format; sample_rate; _ } -> 134 + Format.fprintf fmt "File<%s, %d Hz>" (format_to_string format) sample_rate 135 + | Rtl_tcp_source { frequency; sample_rate; _ } -> 136 + Format.fprintf fmt "rtl_tcp<%d Hz, %d sps>" frequency sample_rate
+168
lib/rtl433_input.mli
··· 1 + (** Input sources for I/Q sample data. 2 + 3 + This module provides an abstraction for different input sources 4 + including files and rtl_tcp network connections. *) 5 + 6 + (** {1 Types} *) 7 + 8 + (** Sample format. *) 9 + type format = Rtl433_baseband.sample_format = 10 + | CU8 (** Complex unsigned 8-bit *) 11 + | CS8 (** Complex signed 8-bit *) 12 + | CS16 (** Complex signed 16-bit *) 13 + | CF32 (** Complex float 32-bit *) 14 + 15 + (** Input source handle. *) 16 + type t 17 + 18 + (** {1 File Input} *) 19 + 20 + (** Open a file as an input source. 21 + 22 + @param sw Eio switch for resource management 23 + @param fs Filesystem capability 24 + @param path File path 25 + @param format Sample format 26 + @param sample_rate Sample rate in Hz 27 + @return Input source *) 28 + val open_file : 29 + sw:Eio.Switch.t -> 30 + fs:_ Eio.Path.t -> 31 + path:string -> 32 + format:format -> 33 + sample_rate:int -> 34 + t 35 + 36 + (** Detect format from filename. 37 + 38 + Examines file extension to determine format: 39 + - .cu8 -> CU8 40 + - .cs8 -> CS8 41 + - .cs16 -> CS16 42 + - .cf32 -> CF32 43 + 44 + @param filename Filename to examine 45 + @return Detected format or None *) 46 + val detect_format : string -> format option 47 + 48 + (** Detect sample rate from filename. 49 + 50 + Looks for patterns like "250k" or "1M" in filename. 51 + 52 + @param filename Filename to examine 53 + @return Detected sample rate or None *) 54 + val detect_sample_rate : string -> int option 55 + 56 + (** {1 RTL-TCP Input} *) 57 + 58 + (** Connect to an rtl_tcp server. 59 + 60 + rtl_tcp is a server that streams I/Q samples over TCP. 61 + This allows using SDR devices on remote machines. 62 + 63 + @param sw Eio switch for resource management 64 + @param net Network capability 65 + @param host Server hostname 66 + @param port Server port (default 1234) 67 + @return Input source *) 68 + val connect_rtl_tcp : 69 + sw:Eio.Switch.t -> 70 + net:_ Eio.Net.t -> 71 + host:string -> 72 + ?port:int -> 73 + unit -> 74 + t 75 + 76 + (** {1 RTL-TCP Commands} *) 77 + 78 + (** Set center frequency on rtl_tcp source. 79 + 80 + @param input Input source (must be rtl_tcp) 81 + @param freq Frequency in Hz *) 82 + val set_frequency : t -> int -> unit 83 + 84 + (** Set sample rate on rtl_tcp source. 85 + 86 + @param input Input source 87 + @param rate Sample rate in Hz *) 88 + val set_sample_rate : t -> int -> unit 89 + 90 + (** Set gain on rtl_tcp source. 91 + 92 + @param input Input source 93 + @param gain Gain in tenths of dB (e.g., 400 = 40.0 dB). 94 + Use 0 for automatic gain. *) 95 + val set_gain : t -> int -> unit 96 + 97 + (** Enable/disable automatic gain control. 98 + 99 + @param input Input source 100 + @param enable True for AGC, false for manual gain *) 101 + val set_agc : t -> bool -> unit 102 + 103 + (** {1 Reading Samples} *) 104 + 105 + (** Read samples from the input source. 106 + 107 + @param input Input source 108 + @param buf Buffer to read into 109 + @return Number of bytes read, 0 for EOF *) 110 + val read : t -> bytes -> int 111 + 112 + (** Read samples with timeout. 113 + 114 + @param input Input source 115 + @param buf Buffer to read into 116 + @param clock Eio clock capability 117 + @param timeout_ms Timeout in milliseconds 118 + @return Number of bytes read, 0 for timeout/EOF *) 119 + val read_timeout : 120 + t -> 121 + bytes -> 122 + clock:_ Eio.Time.clock -> 123 + timeout_ms:float -> 124 + int 125 + 126 + (** {1 Properties} *) 127 + 128 + (** Get the sample rate. 129 + 130 + @param input Input source 131 + @return Sample rate in Hz *) 132 + val sample_rate : t -> int 133 + 134 + (** Get the sample format. 135 + 136 + @param input Input source 137 + @return Sample format *) 138 + val format : t -> format 139 + 140 + (** Get the current center frequency. 141 + 142 + @param input Input source 143 + @return Center frequency in Hz, or 0 if unknown *) 144 + val frequency : t -> int 145 + 146 + (** Check if input source is from a network connection. 147 + 148 + @param input Input source 149 + @return True for rtl_tcp, false for file *) 150 + val is_network : t -> bool 151 + 152 + (** {1 Lifecycle} *) 153 + 154 + (** Close the input source. 155 + 156 + For file inputs, closes the file. 157 + For rtl_tcp, disconnects from the server. 158 + 159 + Note: Also closed automatically when the Eio switch completes. *) 160 + val close : t -> unit 161 + 162 + (** {1 Printing} *) 163 + 164 + (** Pretty-print input source info. *) 165 + val pp : Format.formatter -> t -> unit 166 + 167 + (** Format string for sample format. *) 168 + val format_to_string : format -> string
+33
lib/rtl433_modulation.ml
··· 1 + (* Modulation types *) 2 + 3 + type ook = 4 + | Pulse_pcm 5 + | Pulse_ppm 6 + | Pulse_pwm 7 + | Pulse_manchester 8 + | Pulse_dmc 9 + | Pulse_piwm_raw 10 + | Pulse_piwm_dc 11 + | Pulse_nrzs 12 + 13 + type fsk = Pulse_pcm | Pulse_pwm | Pulse_manchester 14 + 15 + type t = OOK of ook | FSK of fsk 16 + 17 + let pp fmt t = 18 + match t with 19 + | OOK Pulse_pcm -> Format.fprintf fmt "OOK_PULSE_PCM" 20 + | OOK Pulse_ppm -> Format.fprintf fmt "OOK_PULSE_PPM" 21 + | OOK Pulse_pwm -> Format.fprintf fmt "OOK_PULSE_PWM" 22 + | OOK Pulse_manchester -> Format.fprintf fmt "OOK_PULSE_MANCHESTER" 23 + | OOK Pulse_dmc -> Format.fprintf fmt "OOK_PULSE_DMC" 24 + | OOK Pulse_piwm_raw -> Format.fprintf fmt "OOK_PULSE_PIWM_RAW" 25 + | OOK Pulse_piwm_dc -> Format.fprintf fmt "OOK_PULSE_PIWM_DC" 26 + | OOK Pulse_nrzs -> Format.fprintf fmt "OOK_PULSE_NRZS" 27 + | FSK Pulse_pcm -> Format.fprintf fmt "FSK_PULSE_PCM" 28 + | FSK Pulse_pwm -> Format.fprintf fmt "FSK_PULSE_PWM" 29 + | FSK Pulse_manchester -> Format.fprintf fmt "FSK_PULSE_MANCHESTER" 30 + 31 + let to_string t = Format.asprintf "%a" pp t 32 + let is_fsk = function FSK _ -> true | OOK _ -> false 33 + let is_ook = function OOK _ -> true | FSK _ -> false
+65
lib/rtl433_modulation.mli
··· 1 + (** Modulation and coding types for RF signals. 2 + 3 + This module defines the modulation schemes used by wireless sensors. 4 + The term "modulation" here refers to the combination of: 5 + - RF modulation (OOK or FSK onto the carrier) 6 + - Line coding (how bits are encoded as pulses/gaps) *) 7 + 8 + (** {1 OOK Modulation Types} 9 + 10 + On-Off Keying modulates a carrier by turning it on and off. 11 + Different line codings determine how bits map to on/off states. *) 12 + 13 + type ook = 14 + | Pulse_pcm 15 + (** Non-Return-to-Zero: Pulse = 1, No pulse = 0. 16 + Also known as RZ (Return-to-Zero) coding. *) 17 + | Pulse_ppm 18 + (** Pulse Position Modulation: Short gap = 0, Long gap = 1. *) 19 + | Pulse_pwm 20 + (** Pulse Width Modulation: Short pulse = 1, Long pulse = 0. *) 21 + | Pulse_manchester 22 + (** Manchester with leading zero bit. 23 + Rising edge = 0, Falling edge = 1. *) 24 + | Pulse_dmc 25 + (** Differential Manchester Coding. 26 + Level shift within clock cycle. *) 27 + | Pulse_piwm_raw 28 + (** Pulse Interval/Width Modulation (raw). *) 29 + | Pulse_piwm_dc 30 + (** Pulse Interval/Width Modulation (DC balanced). *) 31 + | Pulse_nrzs 32 + (** Non-Return-to-Zero Space coding. *) 33 + 34 + (** {1 FSK Modulation Types} 35 + 36 + Frequency Shift Keying modulates a carrier by shifting its frequency. 37 + F1 and F2 frequencies represent the two symbol states. *) 38 + 39 + type fsk = 40 + | Pulse_pcm 41 + (** FSK with NRZ coding: F1 = 1, F2 = 0. *) 42 + | Pulse_pwm 43 + (** FSK with Pulse Width coding. *) 44 + | Pulse_manchester 45 + (** FSK with Manchester coding. *) 46 + 47 + (** {1 Combined Modulation Type} *) 48 + 49 + type t = 50 + | OOK of ook (** On-Off Keying modulation *) 51 + | FSK of fsk (** Frequency Shift Keying modulation *) 52 + 53 + (** {1 Functions} *) 54 + 55 + (** Pretty-print modulation type. *) 56 + val pp : Format.formatter -> t -> unit 57 + 58 + (** Convert modulation to string. *) 59 + val to_string : t -> string 60 + 61 + (** Check if modulation is FSK-based. *) 62 + val is_fsk : t -> bool 63 + 64 + (** Check if modulation is OOK-based. *) 65 + val is_ook : t -> bool
+98
lib/rtl433_output.ml
··· 1 + (* Output handlers *) 2 + 3 + type file_sink = { 4 + mutable buf : Buffer.t; 5 + flow : Eio.File.rw_ty Eio.Resource.t; 6 + } 7 + 8 + type handler = 9 + | Json_stdout 10 + | Json_file of file_sink 11 + | Log_handler 12 + | Null_handler 13 + | Custom_handler of (Rtl433_data.t -> unit) 14 + | Combined of handler list 15 + 16 + type t = { handler : handler } 17 + 18 + let json_stdout () = { handler = Json_stdout } 19 + 20 + let json_file ~sw ~fs ~path ?append:_ () = 21 + let full_path = Eio.Path.(fs / path) in 22 + let flow = Eio.Path.open_out ~sw ~create:(`If_missing 0o644) full_path in 23 + let buf = Buffer.create 4096 in 24 + { handler = Json_file { buf; flow } } 25 + 26 + let log_output () = { handler = Log_handler } 27 + let null () = { handler = Null_handler } 28 + let custom fn = { handler = Custom_handler fn } 29 + 30 + let combine handlers = 31 + { handler = Combined (List.map (fun t -> t.handler) handlers) } 32 + 33 + let rec output_handler handler data = 34 + match handler with 35 + | Json_stdout -> 36 + let json = Rtl433_data.to_json data in 37 + print_endline json 38 + | Json_file sink -> 39 + let json = Rtl433_data.to_json data in 40 + Buffer.add_string sink.buf json; 41 + Buffer.add_char sink.buf '\n'; 42 + if Buffer.length sink.buf > 4096 then begin 43 + Eio.Flow.write sink.flow [ Cstruct.of_string (Buffer.contents sink.buf) ]; 44 + Buffer.clear sink.buf 45 + end 46 + | Log_handler -> Format.printf "%a@." Rtl433_data.pp data 47 + | Null_handler -> () 48 + | Custom_handler fn -> fn data 49 + | Combined handlers -> List.iter (fun h -> output_handler h data) handlers 50 + 51 + let output t data = output_handler t.handler data 52 + 53 + let rec flush_handler handler = 54 + match handler with 55 + | Json_file sink -> 56 + if Buffer.length sink.buf > 0 then begin 57 + Eio.Flow.write sink.flow [ Cstruct.of_string (Buffer.contents sink.buf) ]; 58 + Buffer.clear sink.buf 59 + end 60 + | Combined handlers -> List.iter flush_handler handlers 61 + | _ -> () 62 + 63 + let flush t = flush_handler t.handler 64 + let close _t = () 65 + 66 + type conversion = Native | SI | Customary 67 + type time_format = Default | ISO | Unix | Unix_usec | Off 68 + 69 + let convert_units conversion data = 70 + match conversion with 71 + | Native -> data 72 + | SI -> data (* TODO: implement conversions *) 73 + | Customary -> data (* TODO: implement conversions *) 74 + 75 + let add_metadata ?time_format ?include_protocol data = 76 + let time_field = 77 + match time_format with 78 + | Some Off | None -> [] 79 + | Some (Default | ISO) -> 80 + let now = Ptime_clock.now () in 81 + [ Rtl433_data.string "time" (Ptime.to_rfc3339 now) ] 82 + | Some Unix -> 83 + let now = Ptime_clock.now () in 84 + let ts = Ptime.to_float_s now in 85 + [ Rtl433_data.float "time" ts ] 86 + | Some Unix_usec -> 87 + let now = Ptime_clock.now () in 88 + let ts = Ptime.to_float_s now in 89 + [ Rtl433_data.float "time" (ts *. 1_000_000.0) ] 90 + in 91 + let protocol_field = 92 + match include_protocol with 93 + | Some true -> [ Rtl433_data.int "protocol" 0 ] 94 + | _ -> [] 95 + in 96 + time_field @ protocol_field @ data 97 + 98 + let pp fmt _t = Format.fprintf fmt "Output"
+118
lib/rtl433_output.mli
··· 1 + (** Output handlers for decoded sensor data. 2 + 3 + This module provides an abstraction for outputting decoded data 4 + to various destinations (JSON, log, file, etc.). *) 5 + 6 + (** {1 Output Handler Interface} *) 7 + 8 + (** An output handler. *) 9 + type t 10 + 11 + (** {1 Construction} *) 12 + 13 + (** Create a JSON output handler to stdout. 14 + 15 + Outputs each decoded message as a single-line JSON object. *) 16 + val json_stdout : unit -> t 17 + 18 + (** Create a JSON output handler to a file. 19 + 20 + @param sw Eio switch for resource management 21 + @param fs Filesystem capability 22 + @param path Output file path 23 + @param append If true, append to existing file *) 24 + val json_file : 25 + sw:Eio.Switch.t -> 26 + fs:_ Eio.Path.t -> 27 + path:string -> 28 + ?append:bool -> 29 + unit -> 30 + t 31 + 32 + (** Create a log-style output handler. 33 + 34 + Outputs human-readable formatted messages. *) 35 + val log_output : unit -> t 36 + 37 + (** Create a null output handler (discards all output). *) 38 + val null : unit -> t 39 + 40 + (** Create a custom output handler. 41 + 42 + @param output_fn Function called for each decoded message *) 43 + val custom : (Rtl433_data.t -> unit) -> t 44 + 45 + (** {1 Multiple Outputs} *) 46 + 47 + (** Combine multiple output handlers. 48 + 49 + Data is sent to all handlers. *) 50 + val combine : t list -> t 51 + 52 + (** {1 Using Outputs} *) 53 + 54 + (** Output a decoded message. 55 + 56 + @param handler Output handler 57 + @param data Decoded data to output *) 58 + val output : t -> Rtl433_data.t -> unit 59 + 60 + (** Flush any buffered output. 61 + 62 + @param handler Output handler *) 63 + val flush : t -> unit 64 + 65 + (** Close the output handler. 66 + 67 + @param handler Output handler *) 68 + val close : t -> unit 69 + 70 + (** {1 Conversion Modes} *) 71 + 72 + (** Unit conversion mode for output. *) 73 + type conversion = 74 + | Native 75 + (** Use native units from decoder (may vary). *) 76 + | SI 77 + (** Convert to SI units (Celsius, m/s, hPa, mm). *) 78 + | Customary 79 + (** Convert to US customary units (Fahrenheit, mph, inHg, in). *) 80 + 81 + (** Apply unit conversion to data. 82 + 83 + @param conversion Conversion mode 84 + @param data Input data 85 + @return Data with converted values *) 86 + val convert_units : conversion -> Rtl433_data.t -> Rtl433_data.t 87 + 88 + (** {1 Metadata} *) 89 + 90 + (** Time format for metadata. *) 91 + type time_format = 92 + | Default 93 + (** Default format from ptime *) 94 + | ISO 95 + (** ISO 8601 format *) 96 + | Unix 97 + (** Unix timestamp *) 98 + | Unix_usec 99 + (** Unix timestamp with microseconds *) 100 + | Off 101 + (** No time metadata *) 102 + 103 + (** Add metadata to decoded data. 104 + 105 + @param time_format Time format to use 106 + @param include_protocol Include protocol number 107 + @param data Input data 108 + @return Data with metadata added *) 109 + val add_metadata : 110 + ?time_format:time_format -> 111 + ?include_protocol:bool -> 112 + Rtl433_data.t -> 113 + Rtl433_data.t 114 + 115 + (** {1 Printing} *) 116 + 117 + (** Pretty-print output handler info. *) 118 + val pp : Format.formatter -> t -> unit
+230
lib/rtl433_output_mqtt.ml
··· 1 + (* MQTT output handler using mqtte library *) 2 + 3 + type topic_style = 4 + | Flat of string 5 + | Per_device of string 6 + | Per_field of string 7 + | Home_assistant of string 8 + | Custom of (Rtl433_data.t -> (string * string) list) 9 + 10 + type config = { 11 + host : string; 12 + port : int; 13 + username : string option; 14 + password : string option; 15 + client_id : string option; 16 + topic_style : topic_style; 17 + retain : bool; 18 + qos : [ `At_most_once | `At_least_once | `Exactly_once ]; 19 + } 20 + 21 + let default_config = 22 + { 23 + host = "localhost"; 24 + port = 1883; 25 + username = None; 26 + password = None; 27 + client_id = None; 28 + topic_style = Per_device "rtl433"; 29 + retain = false; 30 + qos = `At_most_once; 31 + } 32 + 33 + let parse_url url = 34 + (* Parse mqtt://[user:pass@]host[:port][,options] *) 35 + try 36 + let url = 37 + if String.starts_with ~prefix:"mqtt://" url then 38 + String.sub url 7 (String.length url - 7) 39 + else url 40 + in 41 + (* Split by comma for options *) 42 + let parts = String.split_on_char ',' url in 43 + let host_part = List.hd parts in 44 + let options = List.tl parts in 45 + 46 + (* Parse host part: [user:pass@]host[:port] *) 47 + let user, pass, host_port = 48 + match String.split_on_char '@' host_part with 49 + | [ hp ] -> (None, None, hp) 50 + | [ auth; hp ] -> ( 51 + match String.split_on_char ':' auth with 52 + | [ u; p ] -> (Some u, Some p, hp) 53 + | [ u ] -> (Some u, None, hp) 54 + | _ -> (None, None, hp)) 55 + | _ -> (None, None, host_part) 56 + in 57 + 58 + let host, port = 59 + match String.split_on_char ':' host_port with 60 + | [ h; p ] -> (h, int_of_string p) 61 + | [ h ] -> (h, 1883) 62 + | _ -> (host_port, 1883) 63 + in 64 + 65 + (* Parse options *) 66 + let config = ref { default_config with host; port; username = user; password = pass } in 67 + List.iter 68 + (fun opt -> 69 + match String.split_on_char '=' opt with 70 + | [ "retain"; "true" ] -> config := { !config with retain = true } 71 + | [ "retain"; "false" ] -> config := { !config with retain = false } 72 + | [ "qos"; "0" ] -> config := { !config with qos = `At_most_once } 73 + | [ "qos"; "1" ] -> config := { !config with qos = `At_least_once } 74 + | [ "qos"; "2" ] -> config := { !config with qos = `Exactly_once } 75 + | [ "user"; u ] -> config := { !config with username = Some u } 76 + | [ "pass"; p ] -> config := { !config with password = Some p } 77 + | _ -> ()) 78 + options; 79 + 80 + Ok !config 81 + with _ -> Error "Invalid MQTT URL format" 82 + 83 + type t = { 84 + config : config; 85 + mutable client : Mqtte_eio.Client.t option; 86 + mutable messages_sent : int; 87 + mutable errors : int; 88 + } 89 + 90 + let create ~sw ~net ~clock config = 91 + (* Build credentials if username/password provided *) 92 + let credentials = 93 + match (config.username, config.password) with 94 + | Some u, Some p -> Some (`Username_password (u, p)) 95 + | Some u, None -> Some (`Username u) 96 + | None, _ -> None 97 + in 98 + 99 + (* Generate client ID if not provided *) 100 + let client_id = 101 + match config.client_id with 102 + | Some id -> id 103 + | None -> Printf.sprintf "rtl433-%d" (Random.int 100000) 104 + in 105 + 106 + let mqtt_config = Mqtte_eio.Client.{ 107 + (default_config ~client_id ()) with 108 + credentials; 109 + keep_alive = 60; 110 + } in 111 + 112 + (* Try to connect *) 113 + try 114 + let client = Mqtte_eio.Client.connect 115 + ~sw ~net ~clock 116 + ~config:mqtt_config 117 + ~host:config.host 118 + ~port:config.port 119 + () 120 + in 121 + { config; client = Some client; messages_sent = 0; errors = 0 } 122 + with exn -> 123 + Logs.err (fun m -> m "MQTT connection failed: %a" Fmt.exn exn); 124 + { config; client = None; messages_sent = 0; errors = 0 } 125 + 126 + let get_topics config data = 127 + match config.topic_style with 128 + | Flat prefix -> [ (prefix, Rtl433_data.to_json data) ] 129 + | Per_device prefix -> 130 + let model = 131 + match Rtl433_data.get_string "model" data with Some m -> m | None -> "unknown" 132 + in 133 + let id = 134 + match Rtl433_data.get_string "id" data with 135 + | Some i -> i 136 + | None -> ( 137 + match Rtl433_data.get_int "id" data with 138 + | Some i -> string_of_int i 139 + | None -> "0") 140 + in 141 + [ (Printf.sprintf "%s/%s/%s" prefix model id, Rtl433_data.to_json data) ] 142 + | Per_field prefix -> 143 + let model = 144 + match Rtl433_data.get_string "model" data with Some m -> m | None -> "unknown" 145 + in 146 + let id = 147 + match Rtl433_data.get_string "id" data with 148 + | Some i -> i 149 + | None -> ( 150 + match Rtl433_data.get_int "id" data with 151 + | Some i -> string_of_int i 152 + | None -> "0") 153 + in 154 + List.map 155 + (fun field -> 156 + let topic = Printf.sprintf "%s/%s/%s/%s" prefix model id field.Rtl433_data.key in 157 + let payload = 158 + match field.Rtl433_data.value with 159 + | Rtl433_data.Int i -> string_of_int i 160 + | Rtl433_data.Float f -> string_of_float f 161 + | Rtl433_data.String s -> s 162 + | _ -> Rtl433_data.to_json [ field ] 163 + in 164 + (topic, payload)) 165 + data 166 + | Home_assistant _prefix -> 167 + (* TODO: Generate HA discovery config *) 168 + [ ("", Rtl433_data.to_json data) ] 169 + | Custom fn -> fn data 170 + 171 + let output t data = 172 + match t.client with 173 + | None -> 174 + t.errors <- t.errors + 1 175 + | Some client -> 176 + if Mqtte_eio.Client.is_connected client then begin 177 + let topics = get_topics t.config data in 178 + List.iter 179 + (fun (topic, payload) -> 180 + if topic <> "" then begin 181 + try 182 + Mqtte_eio.Client.publish 183 + ~qos:t.config.qos 184 + ~retain:t.config.retain 185 + ~topic 186 + payload 187 + client; 188 + t.messages_sent <- t.messages_sent + 1 189 + with exn -> 190 + Logs.err (fun m -> m "MQTT publish failed: %a" Fmt.exn exn); 191 + t.errors <- t.errors + 1 192 + end) 193 + topics 194 + end else 195 + t.errors <- t.errors + 1 196 + 197 + let close t = 198 + match t.client with 199 + | Some client -> 200 + (try Mqtte_eio.Client.disconnect client with _ -> ()); 201 + t.client <- None 202 + | None -> () 203 + 204 + let is_connected t = 205 + match t.client with 206 + | Some client -> Mqtte_eio.Client.is_connected client 207 + | None -> false 208 + 209 + let get_stats t = 210 + Rtl433_data.make 211 + [ 212 + ("messages_sent", None, Rtl433_data.Int t.messages_sent); 213 + ("errors", None, Rtl433_data.Int t.errors); 214 + ("connected", None, Rtl433_data.String (if is_connected t then "true" else "false")); 215 + ] 216 + 217 + let to_output t = 218 + Rtl433_output.custom (fun data -> output t data) 219 + 220 + let pp_config fmt config = 221 + Format.fprintf fmt "MQTT<%s:%d retain=%b qos=%d>" config.host config.port 222 + config.retain 223 + (match config.qos with 224 + | `At_most_once -> 0 225 + | `At_least_once -> 1 226 + | `Exactly_once -> 2) 227 + 228 + let pp fmt t = 229 + Format.fprintf fmt "%a connected=%b msgs=%d" pp_config t.config (is_connected t) 230 + t.messages_sent
+123
lib/rtl433_output_mqtt.mli
··· 1 + (** MQTT output handler for publishing decoded sensor data. 2 + 3 + This module integrates with the ocaml-mqtte library to publish 4 + decoded sensor data to an MQTT broker. *) 5 + 6 + (** {1 Configuration} *) 7 + 8 + (** MQTT topic style for publishing. *) 9 + type topic_style = 10 + | Flat of string 11 + (** Single topic, all data as JSON. 12 + E.g., "rtl433" -> all messages to "rtl433" *) 13 + | Per_device of string 14 + (** Topic per device model and ID. 15 + E.g., "rtl433" -> "rtl433/Fineoffset-WH51/abc123" *) 16 + | Per_field of string 17 + (** Topic per device and field. 18 + E.g., "rtl433" -> "rtl433/Fineoffset-WH51/abc123/temperature_C" *) 19 + | Home_assistant of string 20 + (** Home Assistant auto-discovery compatible. 21 + Creates config topics and state topics. *) 22 + | Custom of (Rtl433_data.t -> (string * string) list) 23 + (** Custom function returning (topic, payload) pairs. *) 24 + 25 + (** MQTT connection configuration. *) 26 + type config = { 27 + host : string; 28 + (** Broker hostname. *) 29 + port : int; 30 + (** Broker port (default 1883). *) 31 + username : string option; 32 + (** Username for authentication. *) 33 + password : string option; 34 + (** Password for authentication. *) 35 + client_id : string option; 36 + (** Client ID (auto-generated if None). *) 37 + topic_style : topic_style; 38 + (** How to structure topics. *) 39 + retain : bool; 40 + (** Retain flag for published messages. *) 41 + qos : [ `At_most_once | `At_least_once | `Exactly_once ]; 42 + (** Quality of service level. *) 43 + } 44 + 45 + (** Default configuration. *) 46 + val default_config : config 47 + 48 + (** {1 Parsing Configuration} *) 49 + 50 + (** Parse MQTT URL configuration string. 51 + 52 + Format: mqtt://[user:pass@]host[:port][/topic][,options] 53 + 54 + Options: 55 + - retain=true/false 56 + - qos=0/1/2 57 + - topic_style=flat/device/field/ha 58 + 59 + Example: "mqtt://sensor:pass@192.168.1.1:1883,retain=true" 60 + 61 + @param url URL string 62 + @return Parsed config or error message *) 63 + val parse_url : string -> (config, string) result 64 + 65 + (** {1 Output Handler} *) 66 + 67 + (** MQTT output handler. *) 68 + type t 69 + 70 + (** Create an MQTT output handler. 71 + 72 + @param sw Eio switch for resource management 73 + @param net Network capability 74 + @param clock Clock capability 75 + @param config MQTT configuration 76 + @return Output handler *) 77 + val create : 78 + sw:Eio.Switch.t -> 79 + net:_ Eio.Net.t -> 80 + clock:_ Eio.Time.clock -> 81 + config -> 82 + t 83 + 84 + (** Output decoded data to MQTT. 85 + 86 + @param handler MQTT handler 87 + @param data Decoded sensor data *) 88 + val output : t -> Rtl433_data.t -> unit 89 + 90 + (** Close the MQTT connection. 91 + 92 + @param handler MQTT handler *) 93 + val close : t -> unit 94 + 95 + (** Check if connected to broker. 96 + 97 + @param handler MQTT handler 98 + @return True if connected *) 99 + val is_connected : t -> bool 100 + 101 + (** {1 Statistics} *) 102 + 103 + (** Get publishing statistics. 104 + 105 + @param handler MQTT handler 106 + @return Data with message counts, errors, etc. *) 107 + val get_stats : t -> Rtl433_data.t 108 + 109 + (** {1 As Generic Output} *) 110 + 111 + (** Convert to generic output handler. 112 + 113 + @param handler MQTT handler 114 + @return Generic output handler *) 115 + val to_output : t -> Rtl433_output.t 116 + 117 + (** {1 Printing} *) 118 + 119 + (** Pretty-print configuration. *) 120 + val pp_config : Format.formatter -> config -> unit 121 + 122 + (** Pretty-print handler info. *) 123 + val pp : Format.formatter -> t -> unit
+231
lib/rtl433_pipeline.ml
··· 1 + (* Signal processing pipeline *) 2 + 3 + type stats = { 4 + frames_processed : int; 5 + frames_with_signal : int; 6 + pulses_detected : int; 7 + decode_attempts : int; 8 + decode_successes : int; 9 + messages_output : int; 10 + bytes_processed : int64; 11 + running_time_s : float; 12 + } 13 + 14 + type event = 15 + | Signal_detected of Rtl433_pulse_data.t 16 + | Message_decoded of Rtl433_data.t 17 + | Decode_failed of string * Rtl433_device.decode_result 18 + | Frequency_changed of int 19 + | Input_error of exn 20 + | Stopped 21 + 22 + type t = { 23 + config : Rtl433_config.t; 24 + input : Rtl433_input.t; 25 + outputs : Rtl433_output.t list; 26 + registry : Rtl433_registry.t; 27 + mutable running : bool; 28 + mutable stats : stats; 29 + mutable event_callback : (event -> unit) option; 30 + mutable current_frequency : int; 31 + pulse_detector : Rtl433_pulse_detect.t; 32 + } 33 + 34 + let empty_stats = 35 + { 36 + frames_processed = 0; 37 + frames_with_signal = 0; 38 + pulses_detected = 0; 39 + decode_attempts = 0; 40 + decode_successes = 0; 41 + messages_output = 0; 42 + bytes_processed = 0L; 43 + running_time_s = 0.0; 44 + } 45 + 46 + let create ~(config : Rtl433_config.t) ~input ~outputs ~registry = 47 + let sample_rate = Rtl433_input.sample_rate input in 48 + let mode = 49 + (* Determine detection mode from enabled decoders *) 50 + let decoders = Rtl433_registry.enabled registry in 51 + if 52 + List.exists 53 + (fun d -> Rtl433_modulation.is_fsk d.Rtl433_device.modulation) 54 + decoders 55 + then Rtl433_pulse_detect.FSK 56 + else Rtl433_pulse_detect.OOK 57 + in 58 + let pulse_detector = Rtl433_pulse_detect.create ~mode ~sample_rate () in 59 + let current_frequency = 60 + match config.frequencies with f :: _ -> f | [] -> 433920000 61 + in 62 + { 63 + config; 64 + input; 65 + outputs; 66 + registry; 67 + running = false; 68 + stats = empty_stats; 69 + event_callback = None; 70 + current_frequency; 71 + pulse_detector; 72 + } 73 + 74 + let emit_event t event = 75 + match t.event_callback with Some cb -> cb event | None -> () 76 + 77 + let process_pulse_data t pulse_data = 78 + t.stats <- { t.stats with pulses_detected = t.stats.pulses_detected + 1 }; 79 + emit_event t (Signal_detected pulse_data); 80 + 81 + (* Run decoders *) 82 + let results = Rtl433_registry.run_decoders t.registry pulse_data () in 83 + List.iter 84 + (fun (decoder, result) -> 85 + t.stats <- { t.stats with decode_attempts = t.stats.decode_attempts + 1 }; 86 + match result with 87 + | Rtl433_device.Decoded n -> 88 + t.stats <- 89 + { 90 + t.stats with 91 + decode_successes = t.stats.decode_successes + 1; 92 + messages_output = t.stats.messages_output + n; 93 + }; 94 + (* Output decoded data to all outputs *) 95 + (match Rtl433_data.get_last_output () with 96 + | Some data -> 97 + List.iter (fun output -> Rtl433_output.output output data) t.outputs; 98 + emit_event t (Message_decoded data); 99 + Rtl433_data.clear_last_output () 100 + | None -> ()) 101 + | _ -> emit_event t (Decode_failed (decoder.Rtl433_device.name, result))) 102 + results 103 + 104 + let process_buffer t samples = 105 + let len = Bytes.length samples in 106 + t.stats <- 107 + { 108 + t.stats with 109 + frames_processed = t.stats.frames_processed + 1; 110 + bytes_processed = Int64.add t.stats.bytes_processed (Int64.of_int len); 111 + }; 112 + 113 + (* Demodulate *) 114 + let format = Rtl433_input.format t.input in 115 + let envelope = Rtl433_baseband.envelope_detect format samples in 116 + 117 + (* Detect pulses *) 118 + let pulse_datas = Rtl433_pulse_detect.process t.pulse_detector envelope in 119 + if pulse_datas <> [] then 120 + t.stats <- 121 + { t.stats with frames_with_signal = t.stats.frames_with_signal + 1 }; 122 + 123 + (* Process each detected packet *) 124 + List.iter (process_pulse_data t) pulse_datas; 125 + 126 + List.length pulse_datas 127 + 128 + let run t ~clock = 129 + t.running <- true; 130 + let start_time = Eio.Time.now clock in 131 + let buf = Bytes.make (16 * 32 * 512) '\000' in 132 + 133 + while t.running do 134 + try 135 + let n = Rtl433_input.read t.input buf in 136 + if n = 0 then t.running <- false 137 + else begin 138 + let samples = Bytes.sub buf 0 n in 139 + ignore (process_buffer t samples) 140 + end 141 + with 142 + | End_of_file -> 143 + (* Normal end of file, not an error *) 144 + t.running <- false 145 + | exn -> 146 + emit_event t (Input_error exn); 147 + t.running <- false 148 + done; 149 + 150 + (* Flush any remaining pulses *) 151 + (match Rtl433_pulse_detect.flush t.pulse_detector with 152 + | Some pulse_data -> process_pulse_data t pulse_data 153 + | None -> ()); 154 + 155 + let end_time = Eio.Time.now clock in 156 + t.stats <- { t.stats with running_time_s = end_time -. start_time }; 157 + emit_event t Stopped 158 + 159 + let run_for t ~clock ~duration_s = 160 + t.running <- true; 161 + let start_time = Eio.Time.now clock in 162 + let buf = Bytes.make (16 * 32 * 512) '\000' in 163 + 164 + while t.running && Eio.Time.now clock -. start_time < duration_s do 165 + try 166 + let n = 167 + Rtl433_input.read_timeout t.input buf ~clock ~timeout_ms:100.0 168 + in 169 + if n > 0 then begin 170 + let samples = Bytes.sub buf 0 n in 171 + ignore (process_buffer t samples) 172 + end 173 + with exn -> 174 + emit_event t (Input_error exn); 175 + t.running <- false 176 + done; 177 + 178 + t.running <- false; 179 + let end_time = Eio.Time.now clock in 180 + t.stats <- { t.stats with running_time_s = end_time -. start_time }; 181 + emit_event t Stopped 182 + 183 + let stop t = t.running <- false 184 + let is_running t = t.running 185 + 186 + let set_frequency t freq = 187 + Rtl433_input.set_frequency t.input freq; 188 + t.current_frequency <- freq; 189 + emit_event t (Frequency_changed freq) 190 + 191 + let get_frequency t = t.current_frequency 192 + 193 + let enable_hopping _t ~frequencies ~interval_s = 194 + ignore (frequencies, interval_s); 195 + (* TODO: Implement frequency hopping *) 196 + () 197 + 198 + let get_stats t = t.stats 199 + let reset_stats t = t.stats <- empty_stats 200 + 201 + let stats_to_data t = 202 + Rtl433_data.make 203 + [ 204 + ( "frames_processed", 205 + Some "Frames", 206 + Rtl433_data.Int t.stats.frames_processed ); 207 + ("frames_with_signal", Some "Signal", Rtl433_data.Int t.stats.frames_with_signal); 208 + ("pulses_detected", Some "Pulses", Rtl433_data.Int t.stats.pulses_detected); 209 + ("decode_successes", Some "Decoded", Rtl433_data.Int t.stats.decode_successes); 210 + ("messages_output", Some "Messages", Rtl433_data.Int t.stats.messages_output); 211 + ( "bytes_processed", 212 + Some "Bytes", 213 + Rtl433_data.Int (Int64.to_int t.stats.bytes_processed) ); 214 + ( "running_time_s", 215 + Some "Time", 216 + Rtl433_data.Float t.stats.running_time_s ); 217 + ] 218 + 219 + let on_event t callback = t.event_callback <- Some callback 220 + 221 + let rec pp fmt t = 222 + Format.fprintf fmt "Pipeline<%s> running=%b@." (if t.running then "active" else "idle") 223 + t.running; 224 + pp_stats fmt t.stats 225 + 226 + and pp_stats fmt stats = 227 + Format.fprintf fmt 228 + "Stats: frames=%d signal=%d pulses=%d decoded=%d msgs=%d bytes=%Ld time=%.1fs@." 229 + stats.frames_processed stats.frames_with_signal stats.pulses_detected 230 + stats.decode_successes stats.messages_output stats.bytes_processed 231 + stats.running_time_s
+165
lib/rtl433_pipeline.mli
··· 1 + (** Signal processing pipeline. 2 + 3 + This module orchestrates the complete signal processing chain: 4 + Input -> Baseband -> Pulse Detection -> Decoding -> Output *) 5 + 6 + (** {1 Types} *) 7 + 8 + (** Pipeline handle. *) 9 + type t 10 + 11 + (** Pipeline statistics. *) 12 + type stats = { 13 + frames_processed : int; 14 + (** Total frames processed. *) 15 + frames_with_signal : int; 16 + (** Frames with detected signal. *) 17 + pulses_detected : int; 18 + (** Total pulse packages detected. *) 19 + decode_attempts : int; 20 + (** Total decode attempts. *) 21 + decode_successes : int; 22 + (** Successful decodes. *) 23 + messages_output : int; 24 + (** Messages sent to output. *) 25 + bytes_processed : int64; 26 + (** Total bytes processed. *) 27 + running_time_s : float; 28 + (** Total running time in seconds. *) 29 + } 30 + 31 + (** {1 Construction} *) 32 + 33 + (** Create a new pipeline. 34 + 35 + @param config Configuration 36 + @param input Input source 37 + @param outputs Output handlers 38 + @param registry Decoder registry 39 + @return Pipeline handle *) 40 + val create : 41 + config:Rtl433_config.t -> 42 + input:Rtl433_input.t -> 43 + outputs:Rtl433_output.t list -> 44 + registry:Rtl433_registry.t -> 45 + t 46 + 47 + (** {1 Running} *) 48 + 49 + (** Run the pipeline. 50 + 51 + Processes samples from the input, detects pulses, runs decoders, 52 + and sends decoded data to outputs. Runs until input EOF or 53 + stop is called. 54 + 55 + @param pipeline Pipeline handle 56 + @param clock Clock capability for timing *) 57 + val run : t -> clock:_ Eio.Time.clock -> unit 58 + 59 + (** Run the pipeline for a specific duration. 60 + 61 + @param pipeline Pipeline handle 62 + @param clock Clock capability 63 + @param duration_s Maximum duration in seconds *) 64 + val run_for : t -> clock:_ Eio.Time.clock -> duration_s:float -> unit 65 + 66 + (** Process a single buffer of samples. 67 + 68 + Useful for testing or custom integration. 69 + 70 + @param pipeline Pipeline handle 71 + @param samples Sample buffer 72 + @return Number of messages decoded *) 73 + val process_buffer : t -> bytes -> int 74 + 75 + (** {1 Control} *) 76 + 77 + (** Stop the pipeline. 78 + 79 + Signals the pipeline to stop processing. The run function 80 + will return after completing the current buffer. *) 81 + val stop : t -> unit 82 + 83 + (** Check if pipeline is running. 84 + 85 + @param pipeline Pipeline handle 86 + @return True if currently running *) 87 + val is_running : t -> bool 88 + 89 + (** {1 Frequency Control} *) 90 + 91 + (** Set the center frequency. 92 + 93 + For rtl_tcp inputs, sends command to change frequency. 94 + For file inputs, this is a no-op. 95 + 96 + @param pipeline Pipeline handle 97 + @param freq Frequency in Hz *) 98 + val set_frequency : t -> int -> unit 99 + 100 + (** Get the current center frequency. 101 + 102 + @param pipeline Pipeline handle 103 + @return Current frequency in Hz *) 104 + val get_frequency : t -> int 105 + 106 + (** Enable frequency hopping. 107 + 108 + @param pipeline Pipeline handle 109 + @param frequencies List of frequencies in Hz 110 + @param interval_s Hop interval in seconds *) 111 + val enable_hopping : 112 + t -> 113 + frequencies:int list -> 114 + interval_s:float -> 115 + unit 116 + 117 + (** {1 Statistics} *) 118 + 119 + (** Get current statistics. 120 + 121 + @param pipeline Pipeline handle 122 + @return Current statistics *) 123 + val get_stats : t -> stats 124 + 125 + (** Reset statistics. 126 + 127 + @param pipeline Pipeline handle *) 128 + val reset_stats : t -> unit 129 + 130 + (** Get statistics as data structure. 131 + 132 + @param pipeline Pipeline handle 133 + @return Statistics as Rtl433_data.t *) 134 + val stats_to_data : t -> Rtl433_data.t 135 + 136 + (** {1 Events} *) 137 + 138 + (** Event callback type. *) 139 + type event = 140 + | Signal_detected of Rtl433_pulse_data.t 141 + (** Pulse package detected. *) 142 + | Message_decoded of Rtl433_data.t 143 + (** Message successfully decoded. *) 144 + | Decode_failed of string * Rtl433_device.decode_result 145 + (** Decode failed with decoder name and result. *) 146 + | Frequency_changed of int 147 + (** Frequency changed (for hopping). *) 148 + | Input_error of exn 149 + (** Error reading from input. *) 150 + | Stopped 151 + (** Pipeline stopped. *) 152 + 153 + (** Set event callback. 154 + 155 + @param pipeline Pipeline handle 156 + @param callback Function called for each event *) 157 + val on_event : t -> (event -> unit) -> unit 158 + 159 + (** {1 Printing} *) 160 + 161 + (** Pretty-print pipeline status. *) 162 + val pp : Format.formatter -> t -> unit 163 + 164 + (** Pretty-print statistics. *) 165 + val pp_stats : Format.formatter -> stats -> unit
+94
lib/rtl433_pulse_data.ml
··· 1 + (* Pulse timing data *) 2 + 3 + let max_pulses = 1200 4 + let min_pulses = 16 5 + 6 + type t = { 7 + offset : int64; 8 + sample_rate : int; 9 + depth_bits : int; 10 + start_ago : int; 11 + end_ago : int; 12 + num_pulses : int; 13 + pulses : int array; 14 + gaps : int array; 15 + ook_low_estimate : int; 16 + ook_high_estimate : int; 17 + fsk_f1_est : int; 18 + fsk_f2_est : int; 19 + freq1_hz : float; 20 + freq2_hz : float; 21 + centerfreq_hz : float; 22 + range_db : float; 23 + rssi_db : float; 24 + snr_db : float; 25 + noise_db : float; 26 + } 27 + 28 + let create ?(sample_rate = 250000) () = 29 + { 30 + offset = 0L; 31 + sample_rate; 32 + depth_bits = 8; 33 + start_ago = 0; 34 + end_ago = 0; 35 + num_pulses = 0; 36 + pulses = Array.make max_pulses 0; 37 + gaps = Array.make max_pulses 0; 38 + ook_low_estimate = 0; 39 + ook_high_estimate = 0; 40 + fsk_f1_est = 0; 41 + fsk_f2_est = 0; 42 + freq1_hz = 0.0; 43 + freq2_hz = 0.0; 44 + centerfreq_hz = 0.0; 45 + range_db = 0.0; 46 + rssi_db = 0.0; 47 + snr_db = 0.0; 48 + noise_db = 0.0; 49 + } 50 + 51 + let clear t = { t with num_pulses = 0 } 52 + 53 + let add_pulse t ~pulse ~gap = 54 + if t.num_pulses >= max_pulses then None 55 + else begin 56 + t.pulses.(t.num_pulses) <- pulse; 57 + t.gaps.(t.num_pulses) <- gap; 58 + Some { t with num_pulses = t.num_pulses + 1 } 59 + end 60 + 61 + let samples_to_us t samples = 62 + Float.of_int samples *. 1_000_000.0 /. Float.of_int t.sample_rate 63 + 64 + let us_to_samples t us = int_of_float (us *. Float.of_int t.sample_rate /. 1_000_000.0) 65 + 66 + let pulse_us t idx = 67 + if idx < t.num_pulses then samples_to_us t t.pulses.(idx) else 0.0 68 + 69 + let gap_us t idx = 70 + if idx < t.num_pulses then samples_to_us t t.gaps.(idx) else 0.0 71 + 72 + let total_samples t = 73 + let sum = ref 0 in 74 + for i = 0 to t.num_pulses - 1 do 75 + sum := !sum + t.pulses.(i) + t.gaps.(i) 76 + done; 77 + !sum 78 + 79 + let total_us t = samples_to_us t (total_samples t) 80 + 81 + let exceeds_gap_limit t gap_limit_us = 82 + let limit_samples = us_to_samples t gap_limit_us in 83 + let rec check i = 84 + if i >= t.num_pulses then false 85 + else if t.gaps.(i) > limit_samples then true 86 + else check (i + 1) 87 + in 88 + check 0 89 + 90 + let pp fmt t = 91 + Format.fprintf fmt "PulseData: %d pulses, %.1f us total@." t.num_pulses 92 + (total_us t) 93 + 94 + let to_string t = Format.asprintf "%a" pp t
+131
lib/rtl433_pulse_data.mli
··· 1 + (** Pulse timing data from demodulated RF signals. 2 + 3 + This module represents the raw pulse and gap timings extracted 4 + from demodulated RF signals. The pulses and gaps are measured 5 + in sample counts at the configured sample rate. *) 6 + 7 + (** {1 Constants} *) 8 + 9 + (** Maximum number of pulses before forcing end of package. *) 10 + val max_pulses : int 11 + 12 + (** Minimum number of pulses for a valid package. *) 13 + val min_pulses : int 14 + 15 + (** {1 Types} *) 16 + 17 + (** Pulse data containing timing information from a received signal. *) 18 + type t = { 19 + offset : int64; 20 + (** Offset to first pulse in samples from start of stream. *) 21 + sample_rate : int; 22 + (** Sample rate the pulses were recorded at (Hz). *) 23 + depth_bits : int; 24 + (** Sample depth in bits (typically 8). *) 25 + start_ago : int; 26 + (** Start of first pulse in samples ago. *) 27 + end_ago : int; 28 + (** End of last pulse in samples ago. *) 29 + num_pulses : int; 30 + (** Number of pulses recorded. *) 31 + pulses : int array; 32 + (** Width of pulses (high periods) in samples. *) 33 + gaps : int array; 34 + (** Width of gaps (low periods) between pulses in samples. *) 35 + ook_low_estimate : int; 36 + (** Estimated OOK low level (noise floor). *) 37 + ook_high_estimate : int; 38 + (** Estimated OOK high level (signal). *) 39 + fsk_f1_est : int; 40 + (** Estimated F1 frequency for FSK (Hz offset). *) 41 + fsk_f2_est : int; 42 + (** Estimated F2 frequency for FSK (Hz offset). *) 43 + freq1_hz : float; 44 + (** Absolute F1 frequency (Hz). *) 45 + freq2_hz : float; 46 + (** Absolute F2 frequency (Hz). *) 47 + centerfreq_hz : float; 48 + (** Center frequency (Hz). *) 49 + range_db : float; 50 + (** Signal range in dB. *) 51 + rssi_db : float; 52 + (** Received signal strength indicator (dBm). *) 53 + snr_db : float; 54 + (** Signal to noise ratio (dB). *) 55 + noise_db : float; 56 + (** Noise floor (dBm). *) 57 + } 58 + 59 + (** {1 Construction} *) 60 + 61 + (** Create empty pulse data for a given sample rate. 62 + 63 + @param sample_rate Sample rate in Hz (default 250000) 64 + @return Empty pulse data structure *) 65 + val create : ?sample_rate:int -> unit -> t 66 + 67 + (** Clear all pulses from the structure, keeping configuration. *) 68 + val clear : t -> t 69 + 70 + (** {1 Adding Pulses} *) 71 + 72 + (** Add a pulse and gap to the data. 73 + 74 + @param pulse_data The pulse data to modify 75 + @param pulse Pulse width in samples 76 + @param gap Gap width in samples 77 + @return Updated pulse data, or None if max pulses reached *) 78 + val add_pulse : t -> pulse:int -> gap:int -> t option 79 + 80 + (** {1 Timing Conversions} *) 81 + 82 + (** Convert samples to microseconds. 83 + 84 + @param pulse_data Pulse data with sample rate 85 + @param samples Number of samples 86 + @return Time in microseconds *) 87 + val samples_to_us : t -> int -> float 88 + 89 + (** Convert microseconds to samples. 90 + 91 + @param pulse_data Pulse data with sample rate 92 + @param us Time in microseconds 93 + @return Number of samples *) 94 + val us_to_samples : t -> float -> int 95 + 96 + (** Get pulse width in microseconds. 97 + 98 + @param pulse_data Pulse data 99 + @param idx Pulse index 100 + @return Pulse width in microseconds *) 101 + val pulse_us : t -> int -> float 102 + 103 + (** Get gap width in microseconds. 104 + 105 + @param pulse_data Pulse data 106 + @param idx Gap index (same as pulse index) 107 + @return Gap width in microseconds *) 108 + val gap_us : t -> int -> float 109 + 110 + (** {1 Analysis} *) 111 + 112 + (** Calculate total duration in samples. *) 113 + val total_samples : t -> int 114 + 115 + (** Calculate total duration in microseconds. *) 116 + val total_us : t -> float 117 + 118 + (** Check if pulse data exceeds a gap limit. 119 + 120 + @param pulse_data Pulse data 121 + @param gap_limit_us Maximum gap in microseconds 122 + @return True if any gap exceeds the limit *) 123 + val exceeds_gap_limit : t -> float -> bool 124 + 125 + (** {1 Printing} *) 126 + 127 + (** Pretty-print pulse data. *) 128 + val pp : Format.formatter -> t -> unit 129 + 130 + (** Convert to a human-readable string summary. *) 131 + val to_string : t -> string
+187
lib/rtl433_pulse_detect.ml
··· 1 + (* Pulse detection - converts envelope signal to pulse/gap timing *) 2 + 3 + type mode = OOK | FSK 4 + 5 + type state = 6 + | Idle 7 + | In_pulse of int (* samples in current pulse *) 8 + | In_gap of int (* samples in current gap *) 9 + 10 + type t = { 11 + mode : mode; 12 + sample_rate : int; 13 + mutable level_limit : int; 14 + mutable min_pulse_samples : int; 15 + mutable max_gap_samples : int; 16 + mutable reset_gap_samples : int; 17 + mutable level_estimate : int; 18 + mutable noise_estimate : int; 19 + mutable center_frequency : int; 20 + mutable state : state; 21 + mutable current_pulse_data : Rtl433_pulse_data.t; 22 + mutable sample_offset : int64; 23 + } 24 + 25 + let create ~mode ~sample_rate ?(level_limit = 0) () = 26 + let default_level = if level_limit = 0 then 27 + match mode with 28 + | OOK -> 140 (* threshold for OOK above noise *) 29 + | FSK -> 128 (* midpoint for FSK *) 30 + else level_limit 31 + in 32 + { 33 + mode; 34 + sample_rate; 35 + level_limit = default_level; 36 + min_pulse_samples = sample_rate / 50000; (* ~20us min pulse *) 37 + max_gap_samples = sample_rate / 200; (* 5ms max gap within packet *) 38 + reset_gap_samples = sample_rate / 10; (* 100ms reset between packets *) 39 + level_estimate = 128; 40 + noise_estimate = 10; 41 + center_frequency = 0; 42 + state = Idle; 43 + current_pulse_data = Rtl433_pulse_data.create ~sample_rate (); 44 + sample_offset = 0L; 45 + } 46 + 47 + let reset t = 48 + t.state <- Idle; 49 + t.current_pulse_data <- Rtl433_pulse_data.create ~sample_rate:t.sample_rate () 50 + 51 + let finish_packet t = 52 + if t.current_pulse_data.num_pulses >= Rtl433_pulse_data.min_pulses then 53 + let pd = t.current_pulse_data in 54 + t.current_pulse_data <- Rtl433_pulse_data.create ~sample_rate:t.sample_rate (); 55 + Some pd 56 + else begin 57 + t.current_pulse_data <- Rtl433_pulse_data.create ~sample_rate:t.sample_rate (); 58 + None 59 + end 60 + 61 + let _add_pulse_gap t ~pulse ~gap = 62 + if pulse >= t.min_pulse_samples then begin 63 + match Rtl433_pulse_data.add_pulse t.current_pulse_data ~pulse ~gap with 64 + | Some pd -> t.current_pulse_data <- pd; None 65 + | None -> 66 + (* Buffer full, emit packet and start new one *) 67 + let result = finish_packet t in 68 + (match Rtl433_pulse_data.add_pulse t.current_pulse_data ~pulse ~gap with 69 + | Some pd -> t.current_pulse_data <- pd 70 + | None -> ()); 71 + result 72 + end else None 73 + 74 + let process t samples = 75 + let len = Bytes.length samples in 76 + let results = ref [] in 77 + 78 + (* Update level estimate using low-pass filter *) 79 + if len > 0 then begin 80 + let sum = ref 0 in 81 + for i = 0 to min 1000 len - 1 do 82 + sum := !sum + Bytes.get_uint8 samples i 83 + done; 84 + let avg = !sum / min 1000 len in 85 + t.level_estimate <- (t.level_estimate * 7 + avg) / 8 86 + end; 87 + 88 + for i = 0 to len - 1 do 89 + let level = Bytes.get_uint8 samples i in 90 + let is_high = level > t.level_limit in 91 + 92 + match t.state with 93 + | Idle -> 94 + if is_high then 95 + t.state <- In_pulse 1 96 + 97 + | In_pulse pulse_samples -> 98 + if is_high then 99 + t.state <- In_pulse (pulse_samples + 1) 100 + else begin 101 + (* Pulse ended, start counting gap *) 102 + t.state <- In_gap 1; 103 + (* Record pulse width, gap will be filled in later *) 104 + if pulse_samples >= t.min_pulse_samples then begin 105 + t.current_pulse_data.pulses.(t.current_pulse_data.num_pulses) <- pulse_samples 106 + end 107 + end 108 + 109 + | In_gap gap_samples -> 110 + if is_high then begin 111 + (* New pulse started - record the gap from previous pulse *) 112 + if t.current_pulse_data.num_pulses < Rtl433_pulse_data.max_pulses && 113 + t.current_pulse_data.pulses.(t.current_pulse_data.num_pulses) > 0 then begin 114 + t.current_pulse_data.gaps.(t.current_pulse_data.num_pulses) <- gap_samples; 115 + t.current_pulse_data <- { t.current_pulse_data with 116 + num_pulses = t.current_pulse_data.num_pulses + 1 117 + } 118 + end; 119 + t.state <- In_pulse 1 120 + end 121 + else if gap_samples > t.reset_gap_samples then begin 122 + (* Long gap - end of packet *) 123 + (* Record final gap *) 124 + if t.current_pulse_data.num_pulses < Rtl433_pulse_data.max_pulses && 125 + t.current_pulse_data.pulses.(t.current_pulse_data.num_pulses) > 0 then begin 126 + t.current_pulse_data.gaps.(t.current_pulse_data.num_pulses) <- gap_samples; 127 + t.current_pulse_data <- { t.current_pulse_data with 128 + num_pulses = t.current_pulse_data.num_pulses + 1 129 + } 130 + end; 131 + (match finish_packet t with 132 + | Some pd -> results := pd :: !results 133 + | None -> ()); 134 + t.state <- Idle 135 + end 136 + else 137 + t.state <- In_gap (gap_samples + 1) 138 + done; 139 + 140 + t.sample_offset <- Int64.add t.sample_offset (Int64.of_int len); 141 + List.rev !results 142 + 143 + let flush t = 144 + (* Handle any pending pulse *) 145 + (match t.state with 146 + | In_pulse pulse_samples -> 147 + if pulse_samples >= t.min_pulse_samples && 148 + t.current_pulse_data.num_pulses < Rtl433_pulse_data.max_pulses then begin 149 + t.current_pulse_data.pulses.(t.current_pulse_data.num_pulses) <- pulse_samples; 150 + t.current_pulse_data.gaps.(t.current_pulse_data.num_pulses) <- 0; 151 + t.current_pulse_data <- { t.current_pulse_data with 152 + num_pulses = t.current_pulse_data.num_pulses + 1 153 + } 154 + end 155 + | In_gap gap_samples -> 156 + if t.current_pulse_data.num_pulses < Rtl433_pulse_data.max_pulses && 157 + t.current_pulse_data.pulses.(t.current_pulse_data.num_pulses) > 0 then begin 158 + t.current_pulse_data.gaps.(t.current_pulse_data.num_pulses) <- gap_samples; 159 + t.current_pulse_data <- { t.current_pulse_data with 160 + num_pulses = t.current_pulse_data.num_pulses + 1 161 + } 162 + end 163 + | Idle -> ()); 164 + t.state <- Idle; 165 + finish_packet t 166 + 167 + let set_level_limit t level = t.level_limit <- level 168 + let set_min_pulse_samples t samples = t.min_pulse_samples <- samples 169 + 170 + let set_max_gap_ms t ms = 171 + t.max_gap_samples <- int_of_float (ms *. Float.of_int t.sample_rate /. 1000.0) 172 + 173 + let _set_reset_gap_ms t ms = 174 + t.reset_gap_samples <- int_of_float (ms *. Float.of_int t.sample_rate /. 1000.0) 175 + 176 + let get_level_estimate t = t.level_estimate 177 + let get_noise_estimate t = t.noise_estimate 178 + let get_center_frequency t = t.center_frequency 179 + 180 + let pp fmt t = 181 + Format.fprintf fmt "PulseDetect<%s> rate=%d level=%d state=%s" 182 + (match t.mode with OOK -> "OOK" | FSK -> "FSK") 183 + t.sample_rate t.level_limit 184 + (match t.state with 185 + | Idle -> "idle" 186 + | In_pulse n -> Printf.sprintf "pulse(%d)" n 187 + | In_gap n -> Printf.sprintf "gap(%d)" n)
+95
lib/rtl433_pulse_detect.mli
··· 1 + (** Pulse detection from baseband signals. 2 + 3 + This module detects pulses and gaps in demodulated baseband signals, 4 + handling both OOK (amplitude-based) and FSK (frequency-based) signals. *) 5 + 6 + (** {1 Types} *) 7 + 8 + (** Detection mode. *) 9 + type mode = 10 + | OOK 11 + (** On-Off Keying detection based on amplitude threshold. *) 12 + | FSK 13 + (** Frequency Shift Keying detection based on frequency deviation. *) 14 + 15 + (** Pulse detector state. *) 16 + type t 17 + 18 + (** {1 Construction} *) 19 + 20 + (** Create a new pulse detector. 21 + 22 + @param mode Detection mode (OOK or FSK) 23 + @param sample_rate Sample rate in Hz 24 + @param level_limit Minimum signal level (0-255 for 8-bit) 25 + @return New detector instance *) 26 + val create : mode:mode -> sample_rate:int -> ?level_limit:int -> unit -> t 27 + 28 + (** Reset the detector state. 29 + 30 + Clears any partial pulse data and resets level estimates. *) 31 + val reset : t -> unit 32 + 33 + (** {1 Detection} *) 34 + 35 + (** Process samples and detect pulses. 36 + 37 + @param detector The detector 38 + @param samples Baseband samples (envelope for OOK, FM for FSK) 39 + @return List of complete pulse data packages detected *) 40 + val process : t -> bytes -> Rtl433_pulse_data.t list 41 + 42 + (** Force end of current package. 43 + 44 + Call this when the sample stream ends to emit any partial data. 45 + 46 + @param detector The detector 47 + @return Pulse data if any pulses were accumulated, None otherwise *) 48 + val flush : t -> Rtl433_pulse_data.t option 49 + 50 + (** {1 Configuration} *) 51 + 52 + (** Set the signal level threshold for OOK detection. 53 + 54 + @param detector The detector 55 + @param level Level threshold (0-255) *) 56 + val set_level_limit : t -> int -> unit 57 + 58 + (** Set the minimum pulse width. 59 + 60 + Pulses shorter than this are ignored (noise filtering). 61 + 62 + @param detector The detector 63 + @param samples Minimum width in samples *) 64 + val set_min_pulse_samples : t -> int -> unit 65 + 66 + (** Set the maximum gap for end-of-package detection. 67 + 68 + @param detector The detector 69 + @param ms Maximum gap in milliseconds *) 70 + val set_max_gap_ms : t -> float -> unit 71 + 72 + (** {1 Statistics} *) 73 + 74 + (** Get current signal level estimate. 75 + 76 + @param detector The detector 77 + @return Current level estimate *) 78 + val get_level_estimate : t -> int 79 + 80 + (** Get noise floor estimate. 81 + 82 + @param detector The detector 83 + @return Noise floor estimate *) 84 + val get_noise_estimate : t -> int 85 + 86 + (** Get detected center frequency (FSK mode). 87 + 88 + @param detector The detector 89 + @return Center frequency offset, or 0 for OOK *) 90 + val get_center_frequency : t -> int 91 + 92 + (** {1 Printing} *) 93 + 94 + (** Pretty-print detector state. *) 95 + val pp : Format.formatter -> t -> unit
+144
lib/rtl433_pulse_slicer.ml
··· 1 + (* Pulse slicing - convert pulse timing to bits *) 2 + 3 + let pcm pulse_data ~short_width ~tolerance = 4 + let bits = Rtl433_bitbuffer.create () in 5 + let short_samples = Rtl433_pulse_data.us_to_samples pulse_data short_width in 6 + let tol_samples = Rtl433_pulse_data.us_to_samples pulse_data tolerance in 7 + 8 + for i = 0 to pulse_data.Rtl433_pulse_data.num_pulses - 1 do 9 + let pulse = pulse_data.Rtl433_pulse_data.pulses.(i) in 10 + let gap = pulse_data.Rtl433_pulse_data.gaps.(i) in 11 + 12 + (* Count how many bit periods in the pulse *) 13 + let pulse_bits = (pulse + (short_samples / 2)) / short_samples in 14 + let gap_bits = (gap + (short_samples / 2)) / short_samples in 15 + 16 + ignore tol_samples; 17 + 18 + for _ = 1 to pulse_bits do 19 + Rtl433_bitbuffer.add_bit bits 1 20 + done; 21 + for _ = 1 to gap_bits do 22 + Rtl433_bitbuffer.add_bit bits 0 23 + done 24 + done; 25 + bits 26 + 27 + let pwm pulse_data ~short_width ~long_width ~tolerance ?sync_width () = 28 + let bits = Rtl433_bitbuffer.create () in 29 + let short_samples = Rtl433_pulse_data.us_to_samples pulse_data short_width in 30 + let long_samples = Rtl433_pulse_data.us_to_samples pulse_data long_width in 31 + let tol_samples = Rtl433_pulse_data.us_to_samples pulse_data tolerance in 32 + let sync_samples = 33 + match sync_width with 34 + | Some sw -> Rtl433_pulse_data.us_to_samples pulse_data sw 35 + | None -> 0 36 + in 37 + 38 + for i = 0 to pulse_data.Rtl433_pulse_data.num_pulses - 1 do 39 + let pulse = pulse_data.Rtl433_pulse_data.pulses.(i) in 40 + 41 + (* Skip sync pulses *) 42 + if sync_samples > 0 && abs (pulse - sync_samples) < tol_samples then () 43 + else if abs (pulse - short_samples) < tol_samples then 44 + Rtl433_bitbuffer.add_bit bits 1 45 + else if abs (pulse - long_samples) < tol_samples then 46 + Rtl433_bitbuffer.add_bit bits 0 47 + (* else: timing error, skip *) 48 + done; 49 + bits 50 + 51 + let ppm pulse_data ~short_width ~long_width ~tolerance = 52 + let bits = Rtl433_bitbuffer.create () in 53 + let short_samples = Rtl433_pulse_data.us_to_samples pulse_data short_width in 54 + let long_samples = Rtl433_pulse_data.us_to_samples pulse_data long_width in 55 + let tol_samples = Rtl433_pulse_data.us_to_samples pulse_data tolerance in 56 + 57 + for i = 0 to pulse_data.Rtl433_pulse_data.num_pulses - 1 do 58 + let gap = pulse_data.Rtl433_pulse_data.gaps.(i) in 59 + 60 + if abs (gap - short_samples) < tol_samples then 61 + Rtl433_bitbuffer.add_bit bits 0 62 + else if abs (gap - long_samples) < tol_samples then 63 + Rtl433_bitbuffer.add_bit bits 1 64 + (* else: timing error or end marker *) 65 + done; 66 + bits 67 + 68 + let manchester pulse_data ~short_width ~tolerance = 69 + let bits = Rtl433_bitbuffer.create () in 70 + let half_bit = Rtl433_pulse_data.us_to_samples pulse_data short_width in 71 + let full_bit = half_bit * 2 in 72 + let tol_samples = Rtl433_pulse_data.us_to_samples pulse_data tolerance in 73 + 74 + let state = ref 0 in (* 0 = expecting first half, 1 = expecting second *) 75 + 76 + for i = 0 to pulse_data.Rtl433_pulse_data.num_pulses - 1 do 77 + let pulse = pulse_data.Rtl433_pulse_data.pulses.(i) in 78 + let gap = pulse_data.Rtl433_pulse_data.gaps.(i) in 79 + 80 + (* Check pulse width *) 81 + if abs (pulse - half_bit) < tol_samples then begin 82 + if !state = 1 then begin 83 + Rtl433_bitbuffer.add_bit bits 1; 84 + state := 0 85 + end 86 + else state := 1 87 + end 88 + else if abs (pulse - full_bit) < tol_samples then begin 89 + Rtl433_bitbuffer.add_bit bits 0; 90 + state := 0 91 + end; 92 + 93 + (* Check gap width *) 94 + if abs (gap - half_bit) < tol_samples then begin 95 + if !state = 1 then begin 96 + Rtl433_bitbuffer.add_bit bits 0; 97 + state := 0 98 + end 99 + else state := 1 100 + end 101 + else if abs (gap - full_bit) < tol_samples then begin 102 + Rtl433_bitbuffer.add_bit bits 1; 103 + state := 0 104 + end 105 + done; 106 + bits 107 + 108 + let dmc pulse_data ~short_width ~tolerance = 109 + (* Differential Manchester - simplified *) 110 + manchester pulse_data ~short_width ~tolerance 111 + 112 + let slice pulse_data modulation ~short_width ~long_width ~tolerance ?sync_width () = 113 + let open Rtl433_modulation in 114 + match modulation with 115 + | OOK Pulse_pcm | FSK Pulse_pcm -> 116 + pcm pulse_data ~short_width ~tolerance 117 + | OOK Pulse_pwm | FSK Pulse_pwm -> 118 + pwm pulse_data ~short_width ~long_width ~tolerance ?sync_width () 119 + | OOK Pulse_ppm -> 120 + ppm pulse_data ~short_width ~long_width ~tolerance 121 + | OOK Pulse_manchester | FSK Pulse_manchester -> 122 + manchester pulse_data ~short_width ~tolerance 123 + | OOK Pulse_dmc -> 124 + dmc pulse_data ~short_width ~tolerance 125 + | OOK (Pulse_piwm_raw | Pulse_piwm_dc | Pulse_nrzs) -> 126 + (* Fallback to PCM *) 127 + pcm pulse_data ~short_width ~tolerance 128 + 129 + let mark_row_boundaries pulse_data ~gap_limit = 130 + ignore gap_limit; 131 + pulse_data (* TODO: implement row splitting *) 132 + 133 + let find_sync pulse_data ~sync_width ~tolerance = 134 + let sync_samples = Rtl433_pulse_data.us_to_samples pulse_data sync_width in 135 + let tol_samples = Rtl433_pulse_data.us_to_samples pulse_data tolerance in 136 + 137 + let rec find i = 138 + if i >= pulse_data.Rtl433_pulse_data.num_pulses then None 139 + else 140 + let pulse = pulse_data.Rtl433_pulse_data.pulses.(i) in 141 + if abs (pulse - sync_samples) < tol_samples then Some (i + 1) 142 + else find (i + 1) 143 + in 144 + find 0
+138
lib/rtl433_pulse_slicer.mli
··· 1 + (** Pulse slicing: convert pulse timing to bit sequences. 2 + 3 + This module converts pulse and gap timings into bits according 4 + to various encoding schemes (PCM, PWM, PPM, Manchester, etc.). *) 5 + 6 + (** {1 Slicing Functions} *) 7 + 8 + (** Slice pulses using PCM (NRZ) encoding. 9 + 10 + Pulse = 1, No pulse = 0. 11 + Each symbol period is {!short_width} microseconds. 12 + 13 + @param pulse_data Pulse timing data 14 + @param short_width Symbol width in microseconds 15 + @param tolerance Timing tolerance in microseconds 16 + @return Bitbuffer with decoded bits *) 17 + val pcm : 18 + Rtl433_pulse_data.t -> 19 + short_width:float -> 20 + tolerance:float -> 21 + Rtl433_bitbuffer.t 22 + 23 + (** Slice pulses using PWM encoding. 24 + 25 + Short pulse = 1, Long pulse = 0. 26 + Gaps are ignored (fixed width expected). 27 + 28 + @param pulse_data Pulse timing data 29 + @param short_width Short pulse width in microseconds 30 + @param long_width Long pulse width in microseconds 31 + @param tolerance Timing tolerance in microseconds 32 + @param sync_width Optional sync pulse width (ignored if present) 33 + @return Bitbuffer with decoded bits *) 34 + val pwm : 35 + Rtl433_pulse_data.t -> 36 + short_width:float -> 37 + long_width:float -> 38 + tolerance:float -> 39 + ?sync_width:float -> 40 + unit -> 41 + Rtl433_bitbuffer.t 42 + 43 + (** Slice pulses using PPM (Pulse Position Modulation) encoding. 44 + 45 + Short gap = 0, Long gap = 1. 46 + Pulses are expected to be fixed width. 47 + 48 + @param pulse_data Pulse timing data 49 + @param short_width Short gap width in microseconds 50 + @param long_width Long gap width in microseconds 51 + @param tolerance Timing tolerance in microseconds 52 + @return Bitbuffer with decoded bits *) 53 + val ppm : 54 + Rtl433_pulse_data.t -> 55 + short_width:float -> 56 + long_width:float -> 57 + tolerance:float -> 58 + Rtl433_bitbuffer.t 59 + 60 + (** Slice pulses using Manchester encoding. 61 + 62 + Each bit period has a transition in the middle. 63 + High-to-low transition = 0, Low-to-high transition = 1. 64 + 65 + @param pulse_data Pulse timing data 66 + @param short_width Half-bit width in microseconds 67 + @param tolerance Timing tolerance in microseconds 68 + @return Bitbuffer with decoded bits *) 69 + val manchester : 70 + Rtl433_pulse_data.t -> 71 + short_width:float -> 72 + tolerance:float -> 73 + Rtl433_bitbuffer.t 74 + 75 + (** Slice pulses using Differential Manchester encoding. 76 + 77 + Transition at bit boundary = 0, No transition = 1. 78 + 79 + @param pulse_data Pulse timing data 80 + @param short_width Half-bit width in microseconds 81 + @param tolerance Timing tolerance in microseconds 82 + @return Bitbuffer with decoded bits *) 83 + val dmc : 84 + Rtl433_pulse_data.t -> 85 + short_width:float -> 86 + tolerance:float -> 87 + Rtl433_bitbuffer.t 88 + 89 + (** {1 Generic Slicing} *) 90 + 91 + (** Slice pulses according to device timing parameters. 92 + 93 + Automatically selects the appropriate slicer based on modulation type. 94 + 95 + @param pulse_data Pulse timing data 96 + @param modulation Modulation type 97 + @param short_width Short pulse width in microseconds 98 + @param long_width Long pulse width in microseconds 99 + @param tolerance Timing tolerance in microseconds 100 + @param sync_width Optional sync pulse width 101 + @return Bitbuffer with decoded bits *) 102 + val slice : 103 + Rtl433_pulse_data.t -> 104 + Rtl433_modulation.t -> 105 + short_width:float -> 106 + long_width:float -> 107 + tolerance:float -> 108 + ?sync_width:float -> 109 + unit -> 110 + Rtl433_bitbuffer.t 111 + 112 + (** {1 Row Splitting} *) 113 + 114 + (** Split pulse data into rows based on gap threshold. 115 + 116 + Long gaps cause a new row to be started in the bitbuffer. 117 + 118 + @param pulse_data Pulse timing data 119 + @param gap_limit Gap threshold for new row in microseconds 120 + @return Modified pulse data with row boundaries marked *) 121 + val mark_row_boundaries : 122 + Rtl433_pulse_data.t -> 123 + gap_limit:float -> 124 + Rtl433_pulse_data.t 125 + 126 + (** {1 Sync Detection} *) 127 + 128 + (** Find sync pulse pattern in pulse data. 129 + 130 + @param pulse_data Pulse timing data 131 + @param sync_width Expected sync pulse width in microseconds 132 + @param tolerance Timing tolerance 133 + @return Index of first pulse after sync, or None *) 134 + val find_sync : 135 + Rtl433_pulse_data.t -> 136 + sync_width:float -> 137 + tolerance:float -> 138 + int option
+114
lib/rtl433_registry.ml
··· 1 + (* Decoder registry *) 2 + 3 + type filter = All | Enable of int list | Disable of int list 4 + 5 + type t = { 6 + decoders : unit Rtl433_device.t list; 7 + filter : filter; 8 + } 9 + 10 + let create () = { decoders = []; filter = All } 11 + let register t decoder = { t with decoders = decoder :: t.decoders } 12 + let register_all t decoders = { t with decoders = decoders @ t.decoders } 13 + 14 + (* Built-in decoders - registered externally to avoid circular deps *) 15 + let builtin_decoders_ref : (unit -> unit Rtl433_device.t list) ref = ref (fun () -> []) 16 + 17 + let set_builtin_decoders_fn f = builtin_decoders_ref := f 18 + let builtin_decoders () = !builtin_decoders_ref () 19 + let with_builtins () = { decoders = builtin_decoders (); filter = All } 20 + let apply_filter t filter = { t with filter } 21 + 22 + let parse_filter specs = 23 + let enabled = ref [] in 24 + let disabled = ref [] in 25 + List.iter 26 + (fun s -> 27 + match int_of_string_opt s with 28 + | Some n when n < 0 -> disabled := -n :: !disabled 29 + | Some n -> enabled := n :: !enabled 30 + | None -> ()) 31 + specs; 32 + match (!enabled, !disabled) with 33 + | [], [] -> All 34 + | [], ds -> Disable ds 35 + | es, [] -> Enable es 36 + | es, ds -> 37 + (* Enable takes precedence *) 38 + Enable (List.filter (fun e -> not (List.mem e ds)) es) 39 + 40 + let is_enabled t decoder = 41 + if decoder.Rtl433_device.disabled then false 42 + else 43 + match t.filter with 44 + | All -> true 45 + | Enable nums -> List.mem decoder.Rtl433_device.protocol_num nums 46 + | Disable nums -> not (List.mem decoder.Rtl433_device.protocol_num nums) 47 + 48 + let enabled t = List.filter (is_enabled t) t.decoders 49 + 50 + let find_by_num t num = 51 + List.find_opt (fun d -> d.Rtl433_device.protocol_num = num) t.decoders 52 + 53 + let find_by_name t name = 54 + let name_lower = String.lowercase_ascii name in 55 + List.find_opt 56 + (fun d -> String.lowercase_ascii d.Rtl433_device.name = name_lower) 57 + t.decoders 58 + 59 + let count t = List.length t.decoders 60 + let enabled_count t = List.length (enabled t) 61 + 62 + let run_decoders t pulse_data ?(run_all = false) () = 63 + let decoders = enabled t in 64 + let results = ref [] in 65 + let found_success = ref false in 66 + List.iter 67 + (fun d -> 68 + if (not !found_success) || run_all then begin 69 + let result = Rtl433_device.run d pulse_data in 70 + results := (d, result) :: !results; 71 + match result with Rtl433_device.Decoded _ -> found_success := true | _ -> () 72 + end) 73 + decoders; 74 + List.rev !results 75 + 76 + let run_decoders_on_bits t modulation bits ?(run_all = false) () = 77 + let decoders = 78 + List.filter 79 + (fun d -> d.Rtl433_device.modulation = modulation) 80 + (enabled t) 81 + in 82 + let results = ref [] in 83 + let found_success = ref false in 84 + List.iter 85 + (fun d -> 86 + if (not !found_success) || run_all then begin 87 + let result = Rtl433_device.run_on_bits d bits in 88 + results := (d, result) :: !results; 89 + match result with Rtl433_device.Decoded _ -> found_success := true | _ -> () 90 + end) 91 + decoders; 92 + List.rev !results 93 + 94 + let get_stats _t = [] 95 + let reset_stats _t = () 96 + 97 + let pp fmt t = 98 + Format.fprintf fmt "Registry: %d decoders (%d enabled)@." (count t) 99 + (enabled_count t); 100 + List.iter 101 + (fun d -> 102 + let status = if is_enabled t d then "enabled" else "disabled" in 103 + Format.fprintf fmt " %a [%s]@." Rtl433_device.pp d status) 104 + t.decoders 105 + 106 + let list_protocols t = 107 + let buf = Buffer.create 256 in 108 + List.iter 109 + (fun d -> 110 + Buffer.add_string buf 111 + (Printf.sprintf "[%d] %s\n" d.Rtl433_device.protocol_num 112 + d.Rtl433_device.name)) 113 + t.decoders; 114 + Buffer.contents buf
+158
lib/rtl433_registry.mli
··· 1 + (** Decoder registry for managing protocol decoders. 2 + 3 + This module maintains a collection of protocol decoders and handles 4 + protocol filtering (enable/disable). It provides functions to 5 + register decoders and run them against incoming pulse data. *) 6 + 7 + (** {1 Protocol Filtering} *) 8 + 9 + (** Protocol filter specification. *) 10 + type filter = 11 + | All 12 + (** Enable all protocols. *) 13 + | Enable of int list 14 + (** Enable only these protocol numbers. *) 15 + | Disable of int list 16 + (** Disable these protocol numbers (enable all others). *) 17 + 18 + (** {1 Registry} *) 19 + 20 + (** A decoder registry. *) 21 + type t 22 + 23 + (** Create an empty registry. *) 24 + val create : unit -> t 25 + 26 + (** {1 Registration} *) 27 + 28 + (** Register a decoder. 29 + 30 + @param registry The registry 31 + @param decoder Decoder to register 32 + @return Updated registry *) 33 + val register : t -> unit Rtl433_device.t -> t 34 + 35 + (** Register multiple decoders. 36 + 37 + @param registry The registry 38 + @param decoders List of decoders to register 39 + @return Updated registry *) 40 + val register_all : t -> unit Rtl433_device.t list -> t 41 + 42 + (** {1 Built-in Decoders} *) 43 + 44 + (** Set the function that provides built-in decoders. 45 + 46 + This is used to avoid circular dependencies between the registry 47 + and decoder modules. Called during library initialization. 48 + 49 + @param f Function that returns the list of built-in decoders *) 50 + val set_builtin_decoders_fn : (unit -> unit Rtl433_device.t list) -> unit 51 + 52 + (** Get all built-in decoders. 53 + 54 + Returns a list of all decoders compiled into the library. *) 55 + val builtin_decoders : unit -> unit Rtl433_device.t list 56 + 57 + (** Create a registry with all built-in decoders. *) 58 + val with_builtins : unit -> t 59 + 60 + (** {1 Filtering} *) 61 + 62 + (** Apply a filter to the registry. 63 + 64 + @param registry The registry 65 + @param filter Filter specification 66 + @return Updated registry with filter applied *) 67 + val apply_filter : t -> filter -> t 68 + 69 + (** Parse a protocol filter from strings. 70 + 71 + Positive numbers enable, negative numbers disable. 72 + Example: ["142"; "-59"; "-60"] enables 142, disables 59 and 60. 73 + 74 + @param specs List of protocol number strings 75 + @return Filter specification *) 76 + val parse_filter : string list -> filter 77 + 78 + (** {1 Querying} *) 79 + 80 + (** Get all enabled decoders. 81 + 82 + @param registry The registry 83 + @return List of enabled decoders *) 84 + val enabled : t -> unit Rtl433_device.t list 85 + 86 + (** Find a decoder by protocol number. 87 + 88 + @param registry The registry 89 + @param num Protocol number 90 + @return Decoder if found *) 91 + val find_by_num : t -> int -> unit Rtl433_device.t option 92 + 93 + (** Find a decoder by name. 94 + 95 + @param registry The registry 96 + @param name Protocol name (case-insensitive) 97 + @return Decoder if found *) 98 + val find_by_name : t -> string -> unit Rtl433_device.t option 99 + 100 + (** Get total count of registered decoders. *) 101 + val count : t -> int 102 + 103 + (** Get count of enabled decoders. *) 104 + val enabled_count : t -> int 105 + 106 + (** {1 Running Decoders} *) 107 + 108 + (** Run all matching decoders on pulse data. 109 + 110 + Decoders are matched by modulation type and run in priority order. 111 + Returns after the first successful decode, unless run_all is true. 112 + 113 + @param registry The registry 114 + @param pulse_data Pulse timing data 115 + @param run_all If true, run all decoders even after success 116 + @return List of (decoder, result) pairs for decoders that were run *) 117 + val run_decoders : 118 + t -> 119 + Rtl433_pulse_data.t -> 120 + ?run_all:bool -> 121 + unit -> 122 + (unit Rtl433_device.t * Rtl433_device.decode_result) list 123 + 124 + (** Run all matching decoders on a bitbuffer. 125 + 126 + @param registry The registry 127 + @param modulation Modulation type to filter by 128 + @param bits Bitbuffer to decode 129 + @param run_all If true, run all decoders even after success 130 + @return List of (decoder, result) pairs *) 131 + val run_decoders_on_bits : 132 + t -> 133 + Rtl433_modulation.t -> 134 + Rtl433_bitbuffer.t -> 135 + ?run_all:bool -> 136 + unit -> 137 + (unit Rtl433_device.t * Rtl433_device.decode_result) list 138 + 139 + (** {1 Statistics} *) 140 + 141 + (** Get aggregate statistics for all decoders. 142 + 143 + @param registry The registry 144 + @return Data structure with decoder statistics *) 145 + val get_stats : t -> Rtl433_data.t 146 + 147 + (** Reset statistics for all decoders. 148 + 149 + @param registry The registry *) 150 + val reset_stats : t -> unit 151 + 152 + (** {1 Printing} *) 153 + 154 + (** Pretty-print registry contents. *) 155 + val pp : Format.formatter -> t -> unit 156 + 157 + (** List all registered protocols. *) 158 + val list_protocols : t -> string
+129
lib/rtl433_util.ml
··· 1 + (* Utility functions for rtl433 - CRC, bit manipulation, etc. *) 2 + 3 + (* CRC-8 calculation with configurable polynomial and initial value *) 4 + let crc8 ?(poly = 0x31) ?(init = 0x00) data len = 5 + let crc = ref init in 6 + for i = 0 to len - 1 do 7 + let byte = Bytes.get_uint8 data i in 8 + crc := !crc lxor byte; 9 + for _ = 0 to 7 do 10 + if !crc land 0x80 <> 0 then 11 + crc := ((!crc lsl 1) lxor poly) land 0xff 12 + else 13 + crc := (!crc lsl 1) land 0xff 14 + done 15 + done; 16 + !crc 17 + 18 + (* CRC-16 calculation with configurable polynomial and initial value *) 19 + let crc16 ?(poly = 0x8005) ?(init = 0x0000) data len = 20 + let crc = ref init in 21 + for i = 0 to len - 1 do 22 + let byte = Bytes.get_uint8 data i in 23 + crc := !crc lxor (byte lsl 8); 24 + for _ = 0 to 7 do 25 + if !crc land 0x8000 <> 0 then 26 + crc := ((!crc lsl 1) lxor poly) land 0xffff 27 + else 28 + crc := (!crc lsl 1) land 0xffff 29 + done 30 + done; 31 + !crc 32 + 33 + (* XOR all bytes in a range *) 34 + let xor_bytes data offset len = 35 + let result = ref 0 in 36 + for i = offset to offset + len - 1 do 37 + result := !result lxor (Bytes.get_uint8 data i) 38 + done; 39 + !result 40 + 41 + (* Sum all bytes in data *) 42 + let add_bytes data len = 43 + let sum = ref 0 in 44 + for i = 0 to len - 1 do 45 + sum := !sum + Bytes.get_uint8 data i 46 + done; 47 + !sum land 0xff 48 + 49 + (* Get a single bit from byte array (MSB first) *) 50 + let get_bit data bit_idx = 51 + let byte_idx = bit_idx / 8 in 52 + let bit_pos = 7 - (bit_idx mod 8) in 53 + if byte_idx < Bytes.length data then 54 + (Bytes.get_uint8 data byte_idx lsr bit_pos) land 1 55 + else 56 + 0 57 + 58 + (* Get a byte from bit offset (MSB first) *) 59 + let get_byte data ~bit_offset = 60 + let byte_idx = bit_offset / 8 in 61 + let bit_shift = bit_offset mod 8 in 62 + if byte_idx >= Bytes.length data then 0 63 + else if bit_shift = 0 then 64 + Bytes.get_uint8 data byte_idx 65 + else if byte_idx + 1 < Bytes.length data then 66 + let hi = Bytes.get_uint8 data byte_idx in 67 + let lo = Bytes.get_uint8 data (byte_idx + 1) in 68 + ((hi lsl bit_shift) lor (lo lsr (8 - bit_shift))) land 0xff 69 + else 70 + (Bytes.get_uint8 data byte_idx lsl bit_shift) land 0xff 71 + 72 + (* Get multiple bits from bit offset *) 73 + let get_bits data ~bit_offset ~count = 74 + let result = ref 0 in 75 + for i = 0 to count - 1 do 76 + result := (!result lsl 1) lor get_bit data (bit_offset + i) 77 + done; 78 + !result 79 + 80 + (* Reverse bits in a byte *) 81 + let reverse_byte b = 82 + let b = ((b land 0xf0) lsr 4) lor ((b land 0x0f) lsl 4) in 83 + let b = ((b land 0xcc) lsr 2) lor ((b land 0x33) lsl 2) in 84 + ((b land 0xaa) lsr 1) lor ((b land 0x55) lsl 1) 85 + 86 + (* Calculate parity (1 if odd number of 1s, 0 if even) *) 87 + let parity8 b = 88 + let b = b lxor (b lsr 4) in 89 + let b = b lxor (b lsr 2) in 90 + let b = b lxor (b lsr 1) in 91 + b land 1 92 + 93 + (* Reflect bits (reverse bit order within width bits) *) 94 + let reflect data width = 95 + let result = ref 0 in 96 + for i = 0 to width - 1 do 97 + if data land (1 lsl i) <> 0 then 98 + result := !result lor (1 lsl (width - 1 - i)) 99 + done; 100 + !result 101 + 102 + (* Swap nibbles in a byte *) 103 + let swap_nibbles b = 104 + ((b land 0x0f) lsl 4) lor ((b land 0xf0) lsr 4) 105 + 106 + (* Convert bytes to hex string *) 107 + let bytes_to_hex data len = 108 + let hex = Buffer.create (len * 2) in 109 + for i = 0 to len - 1 do 110 + Buffer.add_string hex (Printf.sprintf "%02x" (Bytes.get_uint8 data i)) 111 + done; 112 + Buffer.contents hex 113 + 114 + (* Convert hex string to bytes *) 115 + let hex_to_bytes hex = 116 + let len = String.length hex / 2 in 117 + let data = Bytes.create len in 118 + for i = 0 to len - 1 do 119 + let hi = Char.code hex.[i * 2] in 120 + let lo = Char.code hex.[i * 2 + 1] in 121 + let hex_val c = 122 + if c >= Char.code '0' && c <= Char.code '9' then c - Char.code '0' 123 + else if c >= Char.code 'a' && c <= Char.code 'f' then c - Char.code 'a' + 10 124 + else if c >= Char.code 'A' && c <= Char.code 'F' then c - Char.code 'A' + 10 125 + else 0 126 + in 127 + Bytes.set_uint8 data i ((hex_val hi lsl 4) lor hex_val lo) 128 + done; 129 + data
+105
lib/rtl433_util.mli
··· 1 + (** Utility functions for bit manipulation and checksums. 2 + 3 + This module provides low-level utilities for working with binary data, 4 + including CRC calculations, bit extraction, and byte manipulation. *) 5 + 6 + (** {1 CRC Calculations} *) 7 + 8 + (** Calculate CRC-8 checksum. 9 + 10 + @param poly Polynomial (default 0x31, used by many sensors) 11 + @param init Initial value (default 0x00) 12 + @param data Input bytes 13 + @param len Number of bytes to process 14 + @return CRC-8 checksum *) 15 + val crc8 : ?poly:int -> ?init:int -> bytes -> int -> int 16 + 17 + (** Calculate CRC-16 checksum. 18 + 19 + @param poly Polynomial (default 0x8005) 20 + @param init Initial value (default 0x0000) 21 + @param data Input bytes 22 + @param len Number of bytes to process 23 + @return CRC-16 checksum *) 24 + val crc16 : ?poly:int -> ?init:int -> bytes -> int -> int 25 + 26 + (** XOR all bytes together. 27 + 28 + @param data Input bytes 29 + @param offset Starting offset 30 + @param len Number of bytes to XOR 31 + @return XOR of all bytes *) 32 + val xor_bytes : bytes -> int -> int -> int 33 + 34 + (** Add all bytes together (modulo 256). 35 + 36 + @param data Input bytes 37 + @param len Number of bytes to add 38 + @return Sum of bytes mod 256 *) 39 + val add_bytes : bytes -> int -> int 40 + 41 + (** {1 Bit Manipulation} *) 42 + 43 + (** Get a single bit from a byte array. 44 + 45 + @param data Byte array 46 + @param bit_idx Bit index (0 = MSB of first byte) 47 + @return 0 or 1 *) 48 + val get_bit : bytes -> int -> int 49 + 50 + (** Get a byte from a potentially unaligned bit position. 51 + 52 + @param data Byte array 53 + @param bit_offset Bit offset to start from 54 + @return Byte value *) 55 + val get_byte : bytes -> bit_offset:int -> int 56 + 57 + (** Extract multiple bits as an integer. 58 + 59 + @param data Byte array 60 + @param bit_offset Starting bit offset 61 + @param count Number of bits to extract (max 32) 62 + @return Integer value *) 63 + val get_bits : bytes -> bit_offset:int -> count:int -> int 64 + 65 + (** Reverse the bits in a byte. 66 + 67 + @param b Byte value (0-255) 68 + @return Reversed byte *) 69 + val reverse_byte : int -> int 70 + 71 + (** Calculate parity of a byte. 72 + 73 + @param b Byte value 74 + @return 0 for even parity, 1 for odd *) 75 + val parity8 : int -> int 76 + 77 + (** {1 Byte Operations} *) 78 + 79 + (** Reflect (reverse) bits in a value. 80 + 81 + @param data Value to reflect 82 + @param width Number of bits to reflect 83 + @return Reflected value *) 84 + val reflect : int -> int -> int 85 + 86 + (** Swap nibbles in a byte. 87 + 88 + @param b Byte value 89 + @return Byte with nibbles swapped *) 90 + val swap_nibbles : int -> int 91 + 92 + (** {1 String Formatting} *) 93 + 94 + (** Format bytes as hex string. 95 + 96 + @param data Byte array 97 + @param len Number of bytes 98 + @return Hex string like "0a1b2c" *) 99 + val bytes_to_hex : bytes -> int -> string 100 + 101 + (** Parse hex string to bytes. 102 + 103 + @param hex Hex string 104 + @return Byte array *) 105 + val hex_to_bytes : string -> bytes
+41
rtl433.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "RTL-433 decoder for wireless sensors in OCaml" 4 + description: """ 5 + Pure OCaml implementation of rtl_433, a software decoder for wireless sensors 6 + transmitting in the 433MHz/868MHz/915MHz ISM bands. Supports FSK and OOK 7 + modulation with pluggable protocol decoders and MQTT output.""" 8 + maintainer: ["Anil Madhavapeddy"] 9 + authors: ["Anil Madhavapeddy"] 10 + license: "ISC" 11 + tags: ["sdr" "rtl-sdr" "433mhz" "ism" "mqtt" "sensors"] 12 + homepage: "https://github.com/avsm/ocaml-rtl433" 13 + doc: "https://avsm.github.io/ocaml-rtl433" 14 + bug-reports: "https://github.com/avsm/ocaml-rtl433/issues" 15 + depends: [ 16 + "dune" {>= "3.0"} 17 + "ocaml" {>= "5.1"} 18 + "eio" {>= "1.0"} 19 + "eio_main" 20 + "mqtte" {>= "0.1"} 21 + "cmdliner" {>= "1.2"} 22 + "fmt" {>= "0.9"} 23 + "logs" {>= "0.7"} 24 + "ptime" 25 + "odoc" {with-doc} 26 + ] 27 + build: [ 28 + ["dune" "subst"] {dev} 29 + [ 30 + "dune" 31 + "build" 32 + "-p" 33 + name 34 + "-j" 35 + jobs 36 + "@install" 37 + "@runtest" {with-test} 38 + "@doc" {with-doc} 39 + ] 40 + ] 41 + dev-repo: "git+https://github.com/avsm/ocaml-rtl433.git"
+7
test/dune
··· 1 + (executable 2 + (name test_rtl433) 3 + (libraries rtl433)) 4 + 5 + (executable 6 + (name generate_wh51_sample) 7 + (libraries rtl433))
+107
test/generate_wh51_sample.ml
··· 1 + (* Generate a synthetic WH51 sample file for testing *) 2 + 3 + module Util = Rtl433.Util 4 + 5 + (* Generate FSK PCM signal for a byte sequence *) 6 + let generate_fsk_pcm_signal ~sample_rate ~bit_width_us bytes = 7 + let samples_per_bit = int_of_float (float sample_rate *. bit_width_us /. 1_000_000.0) in 8 + let total_bits = (Bytes.length bytes) * 8 in 9 + let total_samples = total_bits * samples_per_bit in 10 + 11 + (* FSK: represent bits with different frequency deviations *) 12 + (* In CU8 format: samples are unsigned 8-bit I/Q pairs centered at 127 *) 13 + let output = Bytes.create (total_samples * 2) in 14 + 15 + let sample_idx = ref 0 in 16 + let phase = ref 0.0 in 17 + 18 + for byte_idx = 0 to Bytes.length bytes - 1 do 19 + let byte = Bytes.get_uint8 bytes byte_idx in 20 + for bit_idx = 7 downto 0 do 21 + let bit = (byte lsr bit_idx) land 1 in 22 + (* FSK: different frequency for 0 vs 1 *) 23 + let freq = if bit = 1 then 0.15 else 0.05 in 24 + 25 + for _ = 0 to samples_per_bit - 1 do 26 + let i = int_of_float (127.0 +. 100.0 *. cos !phase) in 27 + let q = int_of_float (127.0 +. 100.0 *. sin !phase) in 28 + Bytes.set_uint8 output (!sample_idx * 2) i; 29 + Bytes.set_uint8 output (!sample_idx * 2 + 1) q; 30 + phase := !phase +. freq; 31 + incr sample_idx 32 + done 33 + done 34 + done; 35 + output 36 + 37 + (* Create a valid WH51 packet *) 38 + let create_wh51_packet ~device_id ~moisture ~battery_mv = 39 + let data = Bytes.create 14 in 40 + (* Family code *) 41 + Bytes.set_uint8 data 0 0x51; 42 + (* Device ID (24 bits) *) 43 + Bytes.set_uint8 data 1 ((device_id lsr 16) land 0xff); 44 + Bytes.set_uint8 data 2 ((device_id lsr 8) land 0xff); 45 + Bytes.set_uint8 data 3 (device_id land 0xff); 46 + (* Battery status (OK) + counter *) 47 + Bytes.set_uint8 data 4 0x05; 48 + (* Moisture *) 49 + Bytes.set_uint8 data 5 moisture; 50 + (* AD raw value *) 51 + Bytes.set_uint8 data 6 0x01; 52 + Bytes.set_uint8 data 7 0x00; 53 + (* Boost *) 54 + Bytes.set_uint8 data 8 0x00; 55 + (* Battery voltage raw *) 56 + let battery_raw = battery_mv / 2 in 57 + Bytes.set_uint8 data 9 ((battery_raw lsr 8) land 0xff); 58 + Bytes.set_uint8 data 10 (battery_raw land 0xff); 59 + (* Sequence *) 60 + Bytes.set_uint8 data 11 0x00; 61 + (* CRC-8 *) 62 + let crc = Util.crc8 ~poly:0x31 ~init:0x00 data 12 in 63 + Bytes.set_uint8 data 12 crc; 64 + (* Checksum *) 65 + let sum = Util.add_bytes data 13 in 66 + Bytes.set_uint8 data 13 sum; 67 + data 68 + 69 + let () = 70 + let sample_rate = 250_000 in 71 + let bit_width_us = 58.0 in 72 + 73 + (* Preamble + WH51 packet *) 74 + let preamble = Bytes.of_string "\xAA\x2D\xD4" in 75 + let packet = create_wh51_packet ~device_id:0xABCDEF ~moisture:42 ~battery_mv:2800 in 76 + 77 + (* Combine preamble and packet *) 78 + let full_packet = Bytes.cat preamble packet in 79 + 80 + Printf.printf "Generating WH51 sample file...\n"; 81 + Printf.printf "Packet bytes: "; 82 + for i = 0 to Bytes.length full_packet - 1 do 83 + Printf.printf "%02x " (Bytes.get_uint8 full_packet i) 84 + done; 85 + Printf.printf "\n"; 86 + 87 + (* Generate some silence + packet + silence *) 88 + let silence_samples = 10000 in 89 + let silence = Bytes.make (silence_samples * 2) '\127' in 90 + 91 + let signal = generate_fsk_pcm_signal ~sample_rate ~bit_width_us full_packet in 92 + 93 + Printf.printf "Signal samples: %d\n" (Bytes.length signal / 2); 94 + 95 + (* Write to file *) 96 + let output_path = "test/wh51_sample.cu8" in 97 + let oc = open_out_bin output_path in 98 + output_bytes oc silence; 99 + output_bytes oc signal; 100 + output_bytes oc silence; 101 + close_out oc; 102 + 103 + Printf.printf "Written sample file: %s (%d bytes)\n" 104 + output_path 105 + (Bytes.length silence * 2 + Bytes.length signal); 106 + 107 + Printf.printf "\nTo test: opam exec -- dune exec bin/rtl433_cli.exe -- -r %s -f 868000000 -M -v\n" output_path
+154
test/test_rtl433.ml
··· 1 + (* Tests for rtl433 library *) 2 + 3 + module Util = Rtl433.Util 4 + module Bitbuffer = Rtl433.Bitbuffer 5 + module Data = Rtl433.Data 6 + module Device = Rtl433.Device 7 + 8 + (* Test CRC-8 calculation *) 9 + let test_crc8 () = 10 + let data = Bytes.of_string "\x51\x12\x34\x56\x05\x32\x01\x00\x00\x05\x00\x00" in 11 + let crc = Util.crc8 ~poly:0x31 ~init:0x00 data 12 in 12 + Printf.printf "CRC-8 of test data: 0x%02x\n" crc; 13 + assert (crc >= 0 && crc <= 255) 14 + 15 + (* Test add_bytes checksum *) 16 + let test_add_bytes () = 17 + let data = Bytes.of_string "\x01\x02\x03\x04\x05" in 18 + let sum = Util.add_bytes data 5 in 19 + Printf.printf "Sum of bytes 1+2+3+4+5: %d (expected 15)\n" sum; 20 + assert (sum = 15) 21 + 22 + (* Test bitbuffer operations *) 23 + let test_bitbuffer () = 24 + let bb = Bitbuffer.create () in 25 + (* Add 8 bits to create a row *) 26 + for i = 0 to 7 do 27 + Bitbuffer.add_bit bb (if i mod 2 = 0 then 1 else 0) 28 + done; 29 + Printf.printf "Bitbuffer test: created buffer with %d rows, %d bits in row 0\n" 30 + (Bitbuffer.num_rows bb) 31 + (Bitbuffer.bits_per_row bb 0); 32 + assert (Bitbuffer.num_rows bb > 0); 33 + assert (Bitbuffer.bits_per_row bb 0 = 8) 34 + 35 + (* Add a byte to the bitbuffer, MSB first *) 36 + let add_byte_to_bitbuffer bb byte = 37 + for j = 7 downto 0 do 38 + Bitbuffer.add_bit bb ((byte lsr j) land 1) 39 + done 40 + 41 + (* Create a valid WH51 packet for testing *) 42 + let create_wh51_packet ~device_id ~moisture ~battery_mv = 43 + let data = Bytes.create 14 in 44 + (* Family code *) 45 + Bytes.set_uint8 data 0 0x51; 46 + (* Device ID (24 bits) *) 47 + Bytes.set_uint8 data 1 ((device_id lsr 16) land 0xff); 48 + Bytes.set_uint8 data 2 ((device_id lsr 8) land 0xff); 49 + Bytes.set_uint8 data 3 (device_id land 0xff); 50 + (* Battery status (OK) + counter *) 51 + Bytes.set_uint8 data 4 0x05; 52 + (* Moisture *) 53 + Bytes.set_uint8 data 5 moisture; 54 + (* AD raw value *) 55 + Bytes.set_uint8 data 6 0x01; 56 + Bytes.set_uint8 data 7 0x00; 57 + (* Boost *) 58 + Bytes.set_uint8 data 8 0x00; 59 + (* Battery voltage raw *) 60 + let battery_raw = battery_mv / 2 in 61 + Bytes.set_uint8 data 9 ((battery_raw lsr 8) land 0xff); 62 + Bytes.set_uint8 data 10 (battery_raw land 0xff); 63 + (* Sequence *) 64 + Bytes.set_uint8 data 11 0x00; 65 + (* CRC-8 *) 66 + let crc = Util.crc8 ~poly:0x31 ~init:0x00 data 12 in 67 + Bytes.set_uint8 data 12 crc; 68 + (* Checksum *) 69 + let sum = Util.add_bytes data 13 in 70 + Bytes.set_uint8 data 13 sum; 71 + data 72 + 73 + (* Test WH51 decoder with synthetic packet *) 74 + let test_wh51_decoder () = 75 + Printf.printf "\n=== Testing WH51 decoder ===\n"; 76 + 77 + (* Create a valid WH51 packet *) 78 + let packet = create_wh51_packet ~device_id:0x123456 ~moisture:50 ~battery_mv:3000 in 79 + 80 + Printf.printf "Created WH51 packet: "; 81 + for i = 0 to 13 do 82 + Printf.printf "%02x " (Bytes.get_uint8 packet i) 83 + done; 84 + Printf.printf "\n"; 85 + 86 + (* Create bitbuffer with preamble + packet *) 87 + let bb = Bitbuffer.create () in 88 + 89 + (* Add preamble: 0xAA 0x2D 0xD4 *) 90 + add_byte_to_bitbuffer bb 0xAA; 91 + add_byte_to_bitbuffer bb 0x2D; 92 + add_byte_to_bitbuffer bb 0xD4; 93 + 94 + (* Add packet data *) 95 + for i = 0 to 13 do 96 + add_byte_to_bitbuffer bb (Bytes.get_uint8 packet i) 97 + done; 98 + 99 + Printf.printf "Created bitbuffer with %d rows, %d bits in row 0\n" 100 + (Bitbuffer.num_rows bb) 101 + (Bitbuffer.bits_per_row bb 0); 102 + 103 + (* Create decoder and run it *) 104 + let decoders = Rtl433.builtin_decoders () in 105 + let wh51_decoder = List.hd decoders in 106 + Printf.printf "Running decoder: %s\n" wh51_decoder.Device.name; 107 + 108 + let result = wh51_decoder.Device.decode () bb in 109 + 110 + (match result with 111 + | Device.Decoded n -> 112 + Printf.printf "SUCCESS: Decoded %d message(s)\n" n; 113 + (match Data.get_last_output () with 114 + | Some data -> 115 + Printf.printf "Output JSON: %s\n" (Data.to_json data); 116 + Data.clear_last_output () 117 + | None -> 118 + Printf.printf "WARNING: No output data captured\n") 119 + | Device.Abort_length -> 120 + Printf.printf "FAILED: Abort_length (not enough bits)\n" 121 + | Device.Abort_early -> 122 + Printf.printf "FAILED: Abort_early (no preamble found)\n" 123 + | Device.Fail_sanity -> 124 + Printf.printf "FAILED: Fail_sanity (invalid family code)\n" 125 + | Device.Fail_mic -> 126 + Printf.printf "FAILED: Fail_mic (CRC or checksum error)\n"); 127 + 128 + Printf.printf "=== WH51 decoder test complete ===\n" 129 + 130 + (* Test JSON output *) 131 + let test_json_output () = 132 + Printf.printf "\n=== Testing JSON output ===\n"; 133 + let data = Data.make [ 134 + ("model", None, Data.String "Test-Device"); 135 + ("id", None, Data.String "abc123"); 136 + ("temperature_C", Some "Temperature", Data.Float 23.5); 137 + ("humidity", Some "Humidity", Data.Int 65); 138 + ] in 139 + let json = Data.to_json data in 140 + Printf.printf "JSON output: %s\n" json; 141 + assert (String.length json > 0); 142 + Printf.printf "=== JSON output test complete ===\n" 143 + 144 + let () = 145 + Printf.printf "rtl433 library tests\n"; 146 + Printf.printf "====================\n\n"; 147 + 148 + test_crc8 (); 149 + test_add_bytes (); 150 + test_bitbuffer (); 151 + test_json_output (); 152 + test_wh51_decoder (); 153 + 154 + Printf.printf "\nAll tests passed!\n"
+1
test/wh51_sample.cu8
··· 1 + ���ޜ٪ѷ�ýͰգܔ���w�h�Y�L�H�D�@�<�8�5�1�.�+�)�&�$�"� ���te#W*J3==3I*V#es����� �"�$�&�)�+�.�1�4�8�<�?�L�Y�g�v����ܢհͼ�ǷѪ؜ގ������z�u�p�k�f�a�]�X�S�O�K�>�4�+�#���|m _&Q-D79B/O'S$X"] afkpuz�������!�#�%�'�)�,�/�2�6�9�=�A�E�I�M�R�V�[�_�d�i�n�s�x�}����ݟ׭Ϻ�źϭנݑ���s�e�V�I�E�A�=�9�6�3�/�,�*�'�%�#�!���pb$T,G5;@1L(Z"hw���!�(�0�:�F�S�a�p���ޜ٪ѷ�����ʽ͹дӰլاڣܞݙߔ�����|�m�^�P�D�8�.�&� ���te#W*I3==3I*V#es��� �&�.�8�C�P�^�l�{��ߙڧӴ���˳Ԧژ߉����z�u�p�k�f�a�]�X�S�O�K�F�;�0�(�!���wh!Z(L0@;5F,S$X"] afkpuz������#�*�2�=�I�V�d�s����ݟ׭Ϻ���ɾ̺϶Ҳԭש٤۠ݛޖ���������}�x�s�n�i�d�`�[�V�R�M�I�E�A�=�9�6�3�/�,�*�'�%�#�!����zk ]&O.C87D.P&^ m|��� �!�#�%�(�*�-�0�3�7�:�>�B�F�S�a�p���ޜ٪ѷ�ýͰգܔ�����{�v�r�m�h�c�^�Y�U�P�L�H�D�@�<�8�5�1�.�+�)�&�$�"� �������~ytoje`![#W%R,E6:A0N'[!jx���"�)�1�<�H�U�c�q����ݞجи�Ļί֡ܓ����z�u�p�k�f�a�]�X�S�O�J�F�B�7�-�&� ���rc#U+H4<>2K)X"] afkpuz�������%�,�6�A�M�[�i�x����ۤձ̾�ɾ̺϶Ҳԭש٤۟ݛޖ���������s�d�V�I�=�2�*�#���zk ]&O.C87D.Q&^ m|���#�*�3�>�J�X�f�u����ܡ֯λ�ƹЬ؞ݏ���q�c�U�H�<�1�)�"���yj!['N*I-E/A3=6::6=3A0E-I*N'R%W#[!`ejnsx}������� �&�.�8�C�P�^�l�{��ߙڧӴ���˳Ԧۗ߉�z�k�\�O�B�7�-�%� �����|wrmhc ^"Z#U&Q(L1@;5G,T$ap���%�,�6�A�M�[�i�x����ۤձ̾�ɶҩٛތ�}�n�`�R�E�9�/�'�!���uf"X)K2?<4H+U#cr��� �%�-�7�B�O�S�X�\�a�f�k�p�u�z���������ܡ֯λ�ƹЬ؞ݏ���q�c�U�H�<�1�)�"���xj!['N0A:6E-R%`o}���$�+�5�@�L�Y�h�v����ܣհͼ�ȷѪ؜ގ��p�a�S�F�:�7�4�0�-�*�(�%�#�!� �������|wrmhc ^"Z#U&Q(L+H.D1@4<78;5?2B/G,K)O&T$X"] bfkpuz�������!�#�%�'�*�,�/�2�6�9�=�A�E�I�M�R�`�n�}��ޛ٩Ҷ�¾̲դۖ���x�s�n�i�d�`�[�V�R�M�I�E�A�=�9�/�'�!���uf"X)K2?<4H+U#chmrw|������� �!�#�%�(�*�-�0�4�7�:�>�B�F�J�O�S�a�p���ޜتѷ�üͰգܔ���v�q�l�h�c�^�Y�U�P�L�H�C�@�<�8�.�&� ���se#W*I3==3I*W#ejotx}������� �"�$�+�5�@�L�Y�h�v����ܣհͽ�ȷѳԯ֪ئۡܜޗߓ��������z�u�p�k�f�a�\�X�S�O�J�F�B�>�:�7�3�0�-�*�(�%�#�!� �������|wrmhc ^"Z#U&Q(L+H.D1@4<78;5?2C.G,K)O&T$X"] bfkpuz�������!�#�%�'�*�,�/�2�6�9�=�A�E�I�M�R�V�[�`�d�i�n�s�x�}����������ޛ٩Ҷ�¾̱դۖ���x�i�[�M�A�6�2�/�,�*�'�%�#�!�������zupkfa ]"X$T&O)K,G/B2?5;87<4@1D-H+L(Q&U#Z"^ chmrw|������� �!�#�&�(�*�-�0�4�7�;�>�B�F�J�O�S�X�\�a�f�k�p�u�z���������ߗޜܡۦت֯Գѷλ˿�����ʼ͸дӰլاڢܞݙߔ���������{�v�q�l�h�c�^�Y�U�P�L�H�C�?�<�8�5�1�.�+�)�&�$�"� �������}xsnje`![#W%R'N*I-E0A3=6::6=3A/E-I*N'R%W#[!`ejoty~������� �"�$�&�)�+�.�1�5�8�<�@�D�H�L�P�U�Y�^�c�h�m�q�v�{����������ߙݞܣڧجհӴйͽ�����ȿ˻ηѳԯ֪ئۡܜޗߒ��������z�u�p�k�f�a�\�X�S�O�J�F�B�>�:�7�3�0�-�*�(�%�#�!� �������|wrmhc ^"Z$U&Q(L+H.D1@4<78;5?2C.G,K)O&T$X"] bfkz���#�*�2�=�I�V�d�s����ݟۤ٩׭ԲҶϺ̾�����ɾ̺϶ұդۖ���x�i�[�M�A�6�,�%���zupkfa ]"X$S&O)K,F/B2><4H+U#cr��� �&�-�7�B�O�]�k�z��ߘڦԳ˿�ʴӧڙߊ�{�l�^�P�C�8�.�&� ���se#W*I3==3I*W#et��� �&�.�8�D�P�^�c�h�m�r�w�{����������ߙݞܣڧجհӴйͽ�����ȿ˻ηѳԯ֪٦ۡܜޗߒ��������z�u�p�k�f�a�\�X�S�O�J�F�B�>�:�7�3�0�-�*�(�%�#�!� �������|wrmhc ^"Z$U&P(L+H.D1@4<78;5?2C.G,K)O&T$X"] bfkpuz�������!�#�%�'�*�,�/�3�6�9�=�A�E�I�M�R�V�[�`�d�i�n�s�x�}����������ޛݠۤ٩׭ԲҶϺ̾�����ɾ̺϶ұխש٤۟ݛޖ���������}�x�s�n�i�[�M�A�6�,�%���pa$S,F5;@0L(Z!hw���!�(�0�;�F�S�a�p���ޜتѷ�üͰբܔ���v�g�c�^�Y�U�P�L�G�C�?�<�8�5�1�.�&� ���se#V*I3==3I*W#ety~������� �"�$�&�)�+�.�1�5�8�<�@�D�H�L�P�U�Y�^�c�r����ݞجй�Ļή֡ܒ���u�f�X�J�>�3�*�#���|m ^&P.D78;5?2C.G,K)O&T$X"] bgkpu���!�'�/�9�E�R�`�n�}��ޛ٩Ҷ�¾̱դۖ���x�i�[�M�A�6�,�)�'�%�#�!�������zupkfa ]"X$S'O)K,F/B2>5;97D-Q&_ m|���#�+�4�>�K�X