···11+(* RTL-433 decoder library *)
22+33+module Util = Rtl433_util
44+module Modulation = Rtl433_modulation
55+module Data = Rtl433_data
66+module Bitbuffer = Rtl433_bitbuffer
77+module Pulse_data = Rtl433_pulse_data
88+module Device = Rtl433_device
99+module Flex = Rtl433_flex
1010+module Decoder_fineoffset = Rtl433_decoder_fineoffset
1111+module Registry = Rtl433_registry
1212+module Baseband = Rtl433_baseband
1313+module Pulse_detect = Rtl433_pulse_detect
1414+module Pulse_slicer = Rtl433_pulse_slicer
1515+module Input = Rtl433_input
1616+module Output = Rtl433_output
1717+module Output_mqtt = Rtl433_output_mqtt
1818+module Config = Rtl433_config
1919+module Pipeline = Rtl433_pipeline
2020+2121+(* Register builtin decoders *)
2222+let () =
2323+ Registry.set_builtin_decoders_fn (fun () ->
2424+ Decoder_fineoffset.all_decoders ()
2525+ )
2626+2727+type input = [ `File of string | `Rtl_tcp of string * int ]
2828+2929+type output =
3030+ [ `Json_stdout
3131+ | `Json_file of string
3232+ | `Mqtt of Output_mqtt.config
3333+ | `Log
3434+ | `Custom of (Data.t -> unit) ]
3535+3636+let version = "0.1.0"
3737+3838+let print_version () =
3939+ Printf.printf "rtl433 %s\n" version;
4040+ Printf.printf "OCaml implementation of rtl_433\n";
4141+ Printf.printf "Supported protocols:\n";
4242+ let registry = Registry.with_builtins () in
4343+ print_string (Registry.list_protocols registry)
4444+4545+let builtin_decoders () = Registry.builtin_decoders ()
4646+let find_decoder num = Registry.find_by_num (Registry.with_builtins ()) num
4747+4848+let find_decoder_by_name name =
4949+ Registry.find_by_name (Registry.with_builtins ()) name
5050+5151+let list_decoders () =
5252+ let registry = Registry.with_builtins () in
5353+ List.map
5454+ (fun d ->
5555+ ( d.Device.protocol_num,
5656+ d.Device.name,
5757+ not d.Device.disabled ))
5858+ (Registry.enabled registry)
5959+6060+let run ~sw ~env ~input ?frequency ?sample_rate ?output ?protocols ?duration
6161+ ?config () =
6262+ let fs = Eio.Stdenv.fs env in
6363+ let net = Eio.Stdenv.net env in
6464+ let clock = Eio.Stdenv.clock env in
6565+6666+ (* Use provided config or build from options *)
6767+ let config =
6868+ match config with
6969+ | Some c -> c
7070+ | None ->
7171+ let base = Config.default in
7272+ {
7373+ base with
7474+ frequencies =
7575+ (match frequency with Some f -> [ f ] | None -> base.frequencies);
7676+ sample_rate =
7777+ (match sample_rate with Some s -> s | None -> base.sample_rate);
7878+ protocol_filter =
7979+ (match protocols with Some p -> p | None -> base.protocol_filter);
8080+ duration;
8181+ }
8282+ in
8383+8484+ (* Create input source *)
8585+ let input_source =
8686+ match input with
8787+ | `File path ->
8888+ let format =
8989+ match Input.detect_format path with Some f -> f | None -> Input.CU8
9090+ in
9191+ Input.open_file ~sw ~fs ~path ~format ~sample_rate:config.sample_rate
9292+ | `Rtl_tcp (host, port) -> Input.connect_rtl_tcp ~sw ~net ~host ~port ()
9393+ in
9494+9595+ (* Create outputs *)
9696+ let outputs =
9797+ match output with
9898+ | Some `Json_stdout -> [ Output.json_stdout () ]
9999+ | Some (`Json_file path) -> [ Output.json_file ~sw ~fs ~path () ]
100100+ | Some (`Mqtt mqtt_config) ->
101101+ let mqtt = Output_mqtt.create ~sw ~net ~clock mqtt_config in
102102+ [ Output_mqtt.to_output mqtt ]
103103+ | Some `Log -> [ Output.log_output () ]
104104+ | Some (`Custom fn) -> [ Output.custom fn ]
105105+ | None -> [ Output.json_stdout () ]
106106+ in
107107+108108+ (* Setup registry with filter *)
109109+ let registry =
110110+ let reg = Registry.with_builtins () in
111111+ Registry.apply_filter reg config.protocol_filter
112112+ in
113113+114114+ (* Set frequency if using rtl_tcp *)
115115+ (match input with
116116+ | `Rtl_tcp _ ->
117117+ let freq =
118118+ match config.frequencies with f :: _ -> f | [] -> 433920000
119119+ in
120120+ Input.set_frequency input_source freq
121121+ | `File _ -> ());
122122+123123+ (* Create and run pipeline *)
124124+ let pipeline = Pipeline.create ~config ~input:input_source ~outputs ~registry in
125125+126126+ match config.duration with
127127+ | Some d -> Pipeline.run_for pipeline ~clock ~duration_s:d
128128+ | None -> Pipeline.run pipeline ~clock
129129+130130+let run_with_config ~sw ~env ~config_path =
131131+ let fs = Eio.Stdenv.fs env in
132132+ match Config.parse_file ~fs config_path with
133133+ | Ok config -> (
134134+ match config.input with
135135+ | Some (Config.File path) ->
136136+ run ~sw ~env ~input:(`File path) ~config ()
137137+ | Some (Config.Rtl_tcp (host, port)) ->
138138+ run ~sw ~env ~input:(`Rtl_tcp (host, port)) ~config ()
139139+ | None -> failwith "No input source specified in config")
140140+ | Error msg -> failwith ("Config error: " ^ msg)
+162
lib/rtl433.mli
···11+(** RTL-433 decoder library for wireless sensors.
22+33+ This library decodes wireless sensor transmissions in the
44+ 433MHz/868MHz/915MHz ISM bands. It supports both OOK and FSK
55+ modulation with pluggable protocol decoders.
66+77+ {1 Quick Start}
88+99+ {[
1010+ (* Run with default configuration *)
1111+ Eio_main.run @@ fun env ->
1212+ Eio.Switch.run @@ fun sw ->
1313+ Rtl433.run ~sw ~env
1414+ ~input:(`File "samples.cu8")
1515+ ~frequency:868_000_000
1616+ ~output:`Json_stdout
1717+ ()
1818+ ]}
1919+2020+ {1 Modules}
2121+2222+ Core types:
2323+ - {!Modulation} - OOK/FSK modulation types
2424+ - {!Data} - Hierarchical data structure for decoded messages
2525+ - {!Bitbuffer} - 2D bit buffer for decoded bits
2626+ - {!Pulse_data} - Pulse timing data
2727+2828+ Decoders:
2929+ - {!Device} - Protocol decoder interface
3030+ - {!Flex} - User-defined flexible decoders
3131+ - {!Registry} - Decoder registration and filtering
3232+3333+ Signal processing:
3434+ - {!Baseband} - I/Q to baseband conversion
3535+ - {!Pulse_detect} - Pulse detection
3636+ - {!Pulse_slicer} - Pulse to bit conversion
3737+3838+ I/O:
3939+ - {!Input} - Input sources (file, rtl_tcp)
4040+ - {!Output} - Output handlers
4141+ - {!Output_mqtt} - MQTT output
4242+4343+ High-level:
4444+ - {!Config} - Configuration parsing
4545+ - {!Pipeline} - Signal processing pipeline
4646+*)
4747+4848+(** {1 Re-exported Modules} *)
4949+5050+module Util = Rtl433_util
5151+module Modulation = Rtl433_modulation
5252+module Data = Rtl433_data
5353+module Bitbuffer = Rtl433_bitbuffer
5454+module Pulse_data = Rtl433_pulse_data
5555+module Device = Rtl433_device
5656+module Flex = Rtl433_flex
5757+module Registry = Rtl433_registry
5858+module Baseband = Rtl433_baseband
5959+module Pulse_detect = Rtl433_pulse_detect
6060+module Pulse_slicer = Rtl433_pulse_slicer
6161+module Input = Rtl433_input
6262+module Output = Rtl433_output
6363+module Output_mqtt = Rtl433_output_mqtt
6464+module Config = Rtl433_config
6565+module Pipeline = Rtl433_pipeline
6666+6767+(** {1 High-Level API} *)
6868+6969+(** Input source specification. *)
7070+type input =
7171+ [ `File of string
7272+ (** File path for recorded I/Q samples. *)
7373+ | `Rtl_tcp of string * int
7474+ (** rtl_tcp host and port. *)
7575+ ]
7676+7777+(** Output specification. *)
7878+type output =
7979+ [ `Json_stdout
8080+ (** JSON to stdout. *)
8181+ | `Json_file of string
8282+ (** JSON to file. *)
8383+ | `Mqtt of Output_mqtt.config
8484+ (** MQTT output. *)
8585+ | `Log
8686+ (** Human-readable log. *)
8787+ | `Custom of (Data.t -> unit)
8888+ (** Custom output function. *)
8989+ ]
9090+9191+(** Run the decoder.
9292+9393+ This is the main entry point for simple usage. It creates
9494+ an input source, sets up outputs, and runs the processing
9595+ pipeline until the input is exhausted or duration expires.
9696+9797+ @param sw Eio switch for resource management
9898+ @param env Eio environment
9999+ @param input Input source specification
100100+ @param frequency Center frequency in Hz (default 433920000)
101101+ @param sample_rate Sample rate in Hz (default 250000)
102102+ @param output Output specification (default Json_stdout)
103103+ @param protocols Protocol filter (default All)
104104+ @param duration Maximum duration in seconds (default unlimited)
105105+ @param config Optional configuration (overrides other params) *)
106106+val run :
107107+ sw:Eio.Switch.t ->
108108+ env:< fs : _ Eio.Path.t; net : _ Eio.Net.t; clock : _ Eio.Time.clock; .. > ->
109109+ input:input ->
110110+ ?frequency:int ->
111111+ ?sample_rate:int ->
112112+ ?output:output ->
113113+ ?protocols:Registry.filter ->
114114+ ?duration:float ->
115115+ ?config:Config.t ->
116116+ unit ->
117117+ unit
118118+119119+(** Run with configuration file.
120120+121121+ Loads configuration from a file and runs the decoder.
122122+123123+ @param sw Eio switch
124124+ @param env Eio environment
125125+ @param config_path Path to configuration file *)
126126+val run_with_config :
127127+ sw:Eio.Switch.t ->
128128+ env:< fs : _ Eio.Path.t; net : _ Eio.Net.t; clock : _ Eio.Time.clock; .. > ->
129129+ config_path:string ->
130130+ unit
131131+132132+(** {1 Decoder Registration} *)
133133+134134+(** Get all built-in decoders.
135135+136136+ @return List of all compiled-in decoders *)
137137+val builtin_decoders : unit -> unit Device.t list
138138+139139+(** Find decoder by protocol number.
140140+141141+ @param num Protocol number
142142+ @return Decoder if found *)
143143+val find_decoder : int -> unit Device.t option
144144+145145+(** Find decoder by name.
146146+147147+ @param name Protocol name (case-insensitive)
148148+ @return Decoder if found *)
149149+val find_decoder_by_name : string -> unit Device.t option
150150+151151+(** List all decoder names and numbers.
152152+153153+ @return List of (number, name, enabled) tuples *)
154154+val list_decoders : unit -> (int * string * bool) list
155155+156156+(** {1 Version Info} *)
157157+158158+(** Library version string. *)
159159+val version : string
160160+161161+(** Print version and supported protocols. *)
162162+val print_version : unit -> unit
+148
lib/rtl433_baseband.ml
···11+(* Baseband signal processing *)
22+33+type sample_format = CU8 | CS8 | CS16 | CF32
44+55+let magnitude ~i ~q =
66+ (* Alpha-max-beta-min approximation *)
77+ let ai = abs i in
88+ let aq = abs q in
99+ let max_v = max ai aq in
1010+ let min_v = min ai aq in
1111+ max_v + (min_v / 2)
1212+1313+let magnitude_exact ~i ~q =
1414+ sqrt (Float.of_int ((i * i) + (q * q)))
1515+1616+let envelope_detect format samples =
1717+ let len = Bytes.length samples in
1818+ match format with
1919+ | CU8 ->
2020+ let out_len = len / 2 in
2121+ let out = Bytes.make out_len '\000' in
2222+ for i = 0 to out_len - 1 do
2323+ let i_val = Bytes.get_uint8 samples (i * 2) - 128 in
2424+ let q_val = Bytes.get_uint8 samples ((i * 2) + 1) - 128 in
2525+ let mag = magnitude ~i:i_val ~q:q_val in
2626+ Bytes.set_uint8 out i (min 255 (mag + 128))
2727+ done;
2828+ out
2929+ | CS8 ->
3030+ let out_len = len / 2 in
3131+ let out = Bytes.make out_len '\000' in
3232+ for i = 0 to out_len - 1 do
3333+ let i_val = Bytes.get_int8 samples (i * 2) in
3434+ let q_val = Bytes.get_int8 samples ((i * 2) + 1) in
3535+ let mag = magnitude ~i:i_val ~q:q_val in
3636+ Bytes.set_uint8 out i (min 255 (mag + 128))
3737+ done;
3838+ out
3939+ | CS16 ->
4040+ let out_len = len / 4 in
4141+ let out = Bytes.make out_len '\000' in
4242+ for i = 0 to out_len - 1 do
4343+ let i_val = Bytes.get_int16_le samples (i * 4) in
4444+ let q_val = Bytes.get_int16_le samples ((i * 4) + 2) in
4545+ let mag = magnitude ~i:(i_val / 256) ~q:(q_val / 256) in
4646+ Bytes.set_uint8 out i (min 255 (mag + 128))
4747+ done;
4848+ out
4949+ | CF32 ->
5050+ let out_len = len / 8 in
5151+ let out = Bytes.make out_len '\000' in
5252+ for i = 0 to out_len - 1 do
5353+ let i_val = Int32.to_int (Bytes.get_int32_le samples (i * 8)) in
5454+ let q_val = Int32.to_int (Bytes.get_int32_le samples ((i * 8) + 4)) in
5555+ let mag = magnitude ~i:(i_val / 0x1000000) ~q:(q_val / 0x1000000) in
5656+ Bytes.set_uint8 out i (min 255 (mag + 128))
5757+ done;
5858+ out
5959+6060+let fm_demod _format samples =
6161+ (* Simplified FM demodulation - returns frequency deviation *)
6262+ let len = Bytes.length samples / 2 in
6363+ let out = Bytes.make len '\128' in
6464+ (* TODO: proper FM demod with phase derivative *)
6565+ out
6666+6767+let lowpass ~alpha samples =
6868+ let len = Bytes.length samples in
6969+ let out = Bytes.make len '\000' in
7070+ if len > 0 then begin
7171+ let prev = ref (Float.of_int (Bytes.get_uint8 samples 0)) in
7272+ for i = 0 to len - 1 do
7373+ let curr = Float.of_int (Bytes.get_uint8 samples i) in
7474+ prev := !prev +. (alpha *. (curr -. !prev));
7575+ Bytes.set_uint8 out i (int_of_float !prev)
7676+ done
7777+ end;
7878+ out
7979+8080+let highpass ~alpha samples =
8181+ let len = Bytes.length samples in
8282+ let out = Bytes.make len '\000' in
8383+ if len > 0 then begin
8484+ let prev_in = ref (Float.of_int (Bytes.get_uint8 samples 0)) in
8585+ let prev_out = ref 0.0 in
8686+ for i = 0 to len - 1 do
8787+ let curr = Float.of_int (Bytes.get_uint8 samples i) in
8888+ prev_out := alpha *. (!prev_out +. curr -. !prev_in);
8989+ prev_in := curr;
9090+ Bytes.set_uint8 out i (128 + int_of_float !prev_out)
9191+ done
9292+ end;
9393+ out
9494+9595+let remove_dc samples =
9696+ let len = Bytes.length samples in
9797+ if len = 0 then samples
9898+ else begin
9999+ let sum = ref 0 in
100100+ for i = 0 to len - 1 do
101101+ sum := !sum + Bytes.get_uint8 samples i
102102+ done;
103103+ let dc = !sum / len in
104104+ let out = Bytes.make len '\000' in
105105+ for i = 0 to len - 1 do
106106+ let v = Bytes.get_uint8 samples i - dc + 128 in
107107+ Bytes.set_uint8 out i (max 0 (min 255 v))
108108+ done;
109109+ out
110110+ end
111111+112112+let estimate_ook_levels samples =
113113+ let len = Bytes.length samples in
114114+ if len = 0 then (128, 128)
115115+ else begin
116116+ let min_v = ref 255 in
117117+ let max_v = ref 0 in
118118+ for i = 0 to len - 1 do
119119+ let v = Bytes.get_uint8 samples i in
120120+ if v < !min_v then min_v := v;
121121+ if v > !max_v then max_v := v
122122+ done;
123123+ (!min_v, !max_v)
124124+ end
125125+126126+let estimate_fsk_frequencies _samples = (0, 0) (* TODO *)
127127+let amp_to_db amp = 20.0 *. log10 amp
128128+let db_to_amp db = 10.0 ** (db /. 20.0)
129129+130130+let calculate_rssi samples =
131131+ let len = Bytes.length samples in
132132+ if len = 0 then -100.0
133133+ else begin
134134+ let sum = ref 0.0 in
135135+ for i = 0 to len - 1 do
136136+ let v = Float.of_int (Bytes.get_uint8 samples i - 128) in
137137+ sum := !sum +. (v *. v)
138138+ done;
139139+ let rms = sqrt (!sum /. Float.of_int len) in
140140+ amp_to_db (rms /. 128.0)
141141+ end
142142+143143+let calculate_snr samples ~noise_floor =
144144+ let _, high = estimate_ook_levels samples in
145145+ let signal = high - 128 in
146146+ let noise = noise_floor in
147147+ if noise <= 0 then 0.0
148148+ else amp_to_db (Float.of_int signal /. Float.of_int noise)
+124
lib/rtl433_baseband.mli
···11+(** Baseband signal processing for demodulation.
22+33+ This module provides functions for processing raw I/Q samples
44+ into demodulated baseband signals suitable for pulse detection.
55+ It handles both OOK (amplitude) and FSK (frequency) demodulation. *)
66+77+(** {1 Types} *)
88+99+(** Sample format for I/Q data. *)
1010+type sample_format =
1111+ | CU8
1212+ (** Complex unsigned 8-bit (RTL-SDR native format).
1313+ Values 0-255, 128 is zero. *)
1414+ | CS8
1515+ (** Complex signed 8-bit.
1616+ Values -128 to 127. *)
1717+ | CS16
1818+ (** Complex signed 16-bit little-endian.
1919+ Values -32768 to 32767. *)
2020+ | CF32
2121+ (** Complex float 32-bit.
2222+ Values typically -1.0 to 1.0. *)
2323+2424+(** {1 Magnitude/Envelope Detection} *)
2525+2626+(** Calculate magnitude from I/Q samples (fast approximation).
2727+2828+ Uses the alpha-max-beta-min algorithm for speed.
2929+3030+ @param i In-phase sample
3131+ @param q Quadrature sample
3232+ @return Approximate magnitude *)
3333+val magnitude : i:int -> q:int -> int
3434+3535+(** Calculate exact magnitude using sqrt(i^2 + q^2).
3636+3737+ @param i In-phase sample
3838+ @param q Quadrature sample
3939+ @return Exact magnitude *)
4040+val magnitude_exact : i:int -> q:int -> float
4141+4242+(** Envelope detection for OOK signals.
4343+4444+ Converts I/Q samples to amplitude envelope.
4545+4646+ @param format Sample format
4747+ @param samples Raw I/Q sample bytes
4848+ @return Amplitude envelope as byte array *)
4949+val envelope_detect : sample_format -> bytes -> bytes
5050+5151+(** {1 FM Demodulation} *)
5252+5353+(** FM demodulation for FSK signals.
5454+5555+ Extracts instantaneous frequency from I/Q samples
5656+ using the derivative of phase.
5757+5858+ @param format Sample format
5959+ @param samples Raw I/Q sample bytes
6060+ @return Frequency deviation as signed byte array *)
6161+val fm_demod : sample_format -> bytes -> bytes
6262+6363+(** {1 Filtering} *)
6464+6565+(** Low-pass filter using first-order IIR.
6666+6767+ @param alpha Filter coefficient (0.0-1.0, lower = more filtering)
6868+ @param samples Input samples
6969+ @return Filtered samples *)
7070+val lowpass : alpha:float -> bytes -> bytes
7171+7272+(** High-pass filter using first-order IIR.
7373+7474+ @param alpha Filter coefficient
7575+ @param samples Input samples
7676+ @return Filtered samples *)
7777+val highpass : alpha:float -> bytes -> bytes
7878+7979+(** DC removal filter.
8080+8181+ @param samples Input samples
8282+ @return Samples with DC offset removed *)
8383+val remove_dc : bytes -> bytes
8484+8585+(** {1 Level Estimation} *)
8686+8787+(** Estimate signal levels for OOK.
8888+8989+ @param samples Envelope samples
9090+ @return Tuple of (low_estimate, high_estimate) *)
9191+val estimate_ook_levels : bytes -> int * int
9292+9393+(** Estimate center frequency for FSK.
9494+9595+ @param samples FM demodulated samples
9696+ @return Tuple of (f1_estimate, f2_estimate) *)
9797+val estimate_fsk_frequencies : bytes -> int * int
9898+9999+(** {1 Conversion Utilities} *)
100100+101101+(** Convert amplitude to dB.
102102+103103+ @param amp Linear amplitude
104104+ @return Decibels *)
105105+val amp_to_db : float -> float
106106+107107+(** Convert dB to amplitude.
108108+109109+ @param db Decibels
110110+ @return Linear amplitude *)
111111+val db_to_amp : float -> float
112112+113113+(** Calculate RSSI from samples.
114114+115115+ @param samples Envelope samples
116116+ @return RSSI in dBFS *)
117117+val calculate_rssi : bytes -> float
118118+119119+(** Calculate SNR from samples.
120120+121121+ @param samples Envelope samples
122122+ @param noise_floor Known noise floor
123123+ @return SNR in dB *)
124124+val calculate_snr : bytes -> noise_floor:int -> float
+156
lib/rtl433_bitbuffer.ml
···11+(* Two-dimensional bit buffer *)
22+33+let max_cols = 128
44+let max_rows = 50
55+66+type t = {
77+ mutable num_rows : int;
88+ mutable free_row : int;
99+ bits_per_row : int array;
1010+ syncs_before_row : int array;
1111+ bb : bytes array;
1212+}
1313+1414+let create () =
1515+ {
1616+ num_rows = 0;
1717+ free_row = 0;
1818+ bits_per_row = Array.make max_rows 0;
1919+ syncs_before_row = Array.make max_rows 0;
2020+ bb = Array.init max_rows (fun _ -> Bytes.make max_cols '\000');
2121+ }
2222+2323+let clear t =
2424+ t.num_rows <- 0;
2525+ t.free_row <- 0;
2626+ Array.fill t.bits_per_row 0 max_rows 0;
2727+ Array.fill t.syncs_before_row 0 max_rows 0;
2828+ Array.iter (fun b -> Bytes.fill b 0 max_cols '\000') t.bb
2929+3030+let add_bit t bit =
3131+ if t.free_row < max_rows then begin
3232+ let row = t.free_row in
3333+ let bit_pos = t.bits_per_row.(row) in
3434+ if bit_pos < max_cols * 8 then begin
3535+ let byte_pos = bit_pos / 8 in
3636+ let bit_in_byte = 7 - (bit_pos mod 8) in
3737+ if bit <> 0 then begin
3838+ let b = Bytes.get_uint8 t.bb.(row) byte_pos in
3939+ Bytes.set_uint8 t.bb.(row) byte_pos (b lor (1 lsl bit_in_byte))
4040+ end;
4141+ t.bits_per_row.(row) <- bit_pos + 1;
4242+ if t.num_rows = 0 then t.num_rows <- 1
4343+ end
4444+ end
4545+4646+let add_row t =
4747+ if t.bits_per_row.(t.free_row) > 0 && t.free_row + 1 < max_rows then begin
4848+ t.free_row <- t.free_row + 1;
4949+ t.num_rows <- t.num_rows + 1
5050+ end
5151+5252+let add_sync t =
5353+ if t.bits_per_row.(t.free_row) > 0 then add_row t
5454+ else t.syncs_before_row.(t.free_row) <- t.syncs_before_row.(t.free_row) + 1
5555+5656+let num_rows t = t.num_rows
5757+let bits_per_row t row = if row < max_rows then t.bits_per_row.(row) else 0
5858+5959+let syncs_before_row t row =
6060+ if row < max_rows then t.syncs_before_row.(row) else 0
6161+6262+let get_row t row = if row < max_rows then t.bb.(row) else Bytes.empty
6363+6464+let get_bit t ~row ~bit =
6565+ if row < max_rows && bit < t.bits_per_row.(row) then
6666+ let byte_pos = bit / 8 in
6767+ let bit_in_byte = 7 - (bit mod 8) in
6868+ (Bytes.get_uint8 t.bb.(row) byte_pos lsr bit_in_byte) land 1
6969+ else 0
7070+7171+let get_byte t ~row ~bit_offset =
7272+ if row >= max_rows then 0
7373+ else
7474+ let b1 = Bytes.get_uint8 t.bb.(row) (bit_offset / 8) in
7575+ let b2 =
7676+ if bit_offset / 8 + 1 < max_cols then
7777+ Bytes.get_uint8 t.bb.(row) ((bit_offset / 8) + 1)
7878+ else 0
7979+ in
8080+ let shift = bit_offset mod 8 in
8181+ ((b1 lsl shift) lor (b2 lsr (8 - shift))) land 0xff
8282+8383+let extract_bytes t ~row ~bit_offset ~len =
8484+ let num_bytes = (len + 7) / 8 in
8585+ let result = Bytes.make num_bytes '\000' in
8686+ for i = 0 to num_bytes - 1 do
8787+ Bytes.set_uint8 result i (get_byte t ~row ~bit_offset:(bit_offset + (i * 8)))
8888+ done;
8989+ result
9090+9191+let invert t =
9292+ for row = 0 to t.num_rows - 1 do
9393+ let num_bytes = (t.bits_per_row.(row) + 7) / 8 in
9494+ for i = 0 to num_bytes - 1 do
9595+ Bytes.set_uint8 t.bb.(row) i (lnot (Bytes.get_uint8 t.bb.(row) i) land 0xff)
9696+ done
9797+ done
9898+9999+let nrzs_decode _t = () (* TODO *)
100100+let nrzm_decode _t = () (* TODO *)
101101+102102+let manchester_decode _t ~row ~start ~max =
103103+ ignore (row, start, max);
104104+ (create (), 0)
105105+106106+let differential_manchester_decode _t ~row ~start ~max =
107107+ ignore (row, start, max);
108108+ (create (), 0)
109109+110110+let search t ~row ~start ~pattern ~pattern_bits =
111111+ ignore (pattern, pattern_bits);
112112+ if row < max_rows then t.bits_per_row.(row) else start
113113+114114+let compare_rows t ~row_a ~row_b ~max_bits =
115115+ ignore max_bits;
116116+ if row_a >= max_rows || row_b >= max_rows then 1
117117+ else if t.bits_per_row.(row_a) <> t.bits_per_row.(row_b) then 1
118118+ else Bytes.compare t.bb.(row_a) t.bb.(row_b)
119119+120120+let count_repeats t ~row ~max_bits =
121121+ let count = ref 1 in
122122+ for i = 0 to t.num_rows - 1 do
123123+ if i <> row && compare_rows t ~row_a:row ~row_b:i ~max_bits = 0 then
124124+ incr count
125125+ done;
126126+ !count
127127+128128+let find_repeated_row t ~min_repeats ~min_bits =
129129+ let rec check row =
130130+ if row >= t.num_rows then None
131131+ else if
132132+ t.bits_per_row.(row) >= min_bits
133133+ && count_repeats t ~row ~max_bits:0 >= min_repeats
134134+ then Some row
135135+ else check (row + 1)
136136+ in
137137+ check 0
138138+139139+let find_repeated_prefix t ~min_repeats ~min_bits =
140140+ find_repeated_row t ~min_repeats ~min_bits
141141+142142+let parse t _code = clear t (* TODO: implement parsing *)
143143+144144+let row_to_string t row =
145145+ if row >= t.num_rows then ""
146146+ else
147147+ let bits = t.bits_per_row.(row) in
148148+ let num_bytes = (bits + 7) / 8 in
149149+ let hex = Rtl433_util.bytes_to_hex t.bb.(row) num_bytes in
150150+ Printf.sprintf "{%d} %s" bits hex
151151+152152+let pp fmt t =
153153+ Format.fprintf fmt "Bitbuffer: %d rows@." t.num_rows;
154154+ for i = 0 to t.num_rows - 1 do
155155+ Format.fprintf fmt " [%d] %s@." i (row_to_string t i)
156156+ done
+194
lib/rtl433_bitbuffer.mli
···11+(** Two-dimensional bit buffer for storing decoded pulse data.
22+33+ A bitbuffer stores multiple rows of bits, where each row represents
44+ a separate packet or transmission. This is useful for sensors that
55+ send repeated transmissions of the same data. *)
66+77+(** {1 Constants} *)
88+99+(** Maximum number of bytes per row. *)
1010+val max_cols : int
1111+1212+(** Maximum number of rows. *)
1313+val max_rows : int
1414+1515+(** {1 Types} *)
1616+1717+(** A bit buffer containing multiple rows of bits. *)
1818+type t
1919+2020+(** {1 Construction} *)
2121+2222+(** Create a new empty bitbuffer. *)
2323+val create : unit -> t
2424+2525+(** Clear all data from the bitbuffer. *)
2626+val clear : t -> unit
2727+2828+(** {1 Adding Data} *)
2929+3030+(** Add a single bit (0 or 1) to the current row.
3131+3232+ Bits are added MSB first within each byte.
3333+3434+ @param bits The bitbuffer
3535+ @param bit 0 or 1 *)
3636+val add_bit : t -> int -> unit
3737+3838+(** Start a new row in the bitbuffer.
3939+4040+ The current row is finalized and a new empty row is started. *)
4141+val add_row : t -> unit
4242+4343+(** Add a sync marker and optionally start a new row.
4444+4545+ If the current row has data, starts a new row. Otherwise,
4646+ increments the sync counter for the current row. *)
4747+val add_sync : t -> unit
4848+4949+(** {1 Accessing Data} *)
5050+5151+(** Get the number of active rows. *)
5252+val num_rows : t -> int
5353+5454+(** Get the number of bits in a specific row.
5555+5656+ @param bits The bitbuffer
5757+ @param row Row index (0-based)
5858+ @return Number of bits in the row *)
5959+val bits_per_row : t -> int -> int
6060+6161+(** Get the number of sync pulses before a row.
6262+6363+ @param bits The bitbuffer
6464+ @param row Row index
6565+ @return Number of sync pulses *)
6666+val syncs_before_row : t -> int -> int
6767+6868+(** Get the raw bytes of a row.
6969+7070+ @param bits The bitbuffer
7171+ @param row Row index
7272+ @return Bytes containing the row data *)
7373+val get_row : t -> int -> bytes
7474+7575+(** Get a single bit from a specific position.
7676+7777+ @param bits The bitbuffer
7878+ @param row Row index
7979+ @param bit Bit index within the row
8080+ @return 0 or 1 *)
8181+val get_bit : t -> row:int -> bit:int -> int
8282+8383+(** Get a byte from a potentially unaligned bit position.
8484+8585+ @param bits The bitbuffer
8686+ @param row Row index
8787+ @param bit_offset Bit offset within the row
8888+ @return Byte value *)
8989+val get_byte : t -> row:int -> bit_offset:int -> int
9090+9191+(** Extract bytes from a row, handling unaligned access.
9292+9393+ @param bits The bitbuffer
9494+ @param row Row index
9595+ @param bit_offset Starting bit offset
9696+ @param len Number of bits to extract
9797+ @return Extracted bytes *)
9898+val extract_bytes : t -> row:int -> bit_offset:int -> len:int -> bytes
9999+100100+(** {1 Transformations} *)
101101+102102+(** Invert all bits in the bitbuffer (0 becomes 1 and vice versa). *)
103103+val invert : t -> unit
104104+105105+(** Non-Return-to-Zero Space (NRZI) decode the bitbuffer.
106106+107107+ "One" is represented by no change in level,
108108+ "Zero" is represented by change in level. *)
109109+val nrzs_decode : t -> unit
110110+111111+(** Non-Return-to-Zero Mark (NRZM) decode the bitbuffer.
112112+113113+ "One" is represented by change in level,
114114+ "Zero" is represented by no change in level. *)
115115+val nrzm_decode : t -> unit
116116+117117+(** Manchester decode from one bitbuffer into another.
118118+119119+ Decodes at most [max] data bits (2*max input bits) from the
120120+ specified row and starting position. Manchester per IEEE 802.3:
121121+ high-low is 0, low-high is 1.
122122+123123+ @param inbuf Input bitbuffer
124124+ @param row Row to decode from
125125+ @param start Starting bit position
126126+ @param max Maximum data bits to decode
127127+ @return Tuple of (output bitbuffer, input bit position after decode) *)
128128+val manchester_decode : t -> row:int -> start:int -> max:int -> t * int
129129+130130+(** Differential Manchester decode. *)
131131+val differential_manchester_decode : t -> row:int -> start:int -> max:int -> t * int
132132+133133+(** {1 Searching} *)
134134+135135+(** Search for a bit pattern in a row.
136136+137137+ @param bits The bitbuffer
138138+ @param row Row to search
139139+ @param start Starting bit position
140140+ @param pattern Pattern bytes to search for (MSB aligned)
141141+ @param pattern_bits Number of bits in the pattern
142142+ @return Bit position of match, or end of row if not found *)
143143+val search : t -> row:int -> start:int -> pattern:bytes -> pattern_bits:int -> int
144144+145145+(** {1 Row Comparison} *)
146146+147147+(** Compare two rows for equality.
148148+149149+ @param bits The bitbuffer
150150+ @param row_a First row index
151151+ @param row_b Second row index
152152+ @param max_bits Maximum bits to compare (0 = all)
153153+ @return 0 if equal, non-zero otherwise *)
154154+val compare_rows : t -> row_a:int -> row_b:int -> max_bits:int -> int
155155+156156+(** Count how many times a row is repeated.
157157+158158+ @param bits The bitbuffer
159159+ @param row Row to check
160160+ @param max_bits Maximum bits to compare
161161+ @return Number of identical rows (at least 1) *)
162162+val count_repeats : t -> row:int -> max_bits:int -> int
163163+164164+(** Find a row that appears at least min_repeats times.
165165+166166+ @param bits The bitbuffer
167167+ @param min_repeats Minimum number of repetitions
168168+ @param min_bits Minimum row length in bits
169169+ @return Row index or None if not found *)
170170+val find_repeated_row : t -> min_repeats:int -> min_bits:int -> int option
171171+172172+(** Find a repeated row by prefix matching.
173173+174174+ Like [find_repeated_row] but only compares up to [min_bits] bits. *)
175175+val find_repeated_prefix : t -> min_repeats:int -> min_bits:int -> int option
176176+177177+(** {1 Parsing} *)
178178+179179+(** Parse a hex string into a bitbuffer.
180180+181181+ The string may be prefixed with "0x" and rows can be separated
182182+ by "/" or have length prefixes in braces like "{24}".
183183+184184+ @param bits Output bitbuffer (will be cleared first)
185185+ @param code Hex string to parse *)
186186+val parse : t -> string -> unit
187187+188188+(** {1 Printing} *)
189189+190190+(** Pretty-print the bitbuffer contents. *)
191191+val pp : Format.formatter -> t -> unit
192192+193193+(** Format a single row as a hex string with bit count. *)
194194+val row_to_string : t -> int -> string
+173
lib/rtl433_config.ml
···11+(* Configuration *)
22+33+type conversion = Rtl433_output.conversion = Native | SI | Customary
44+type time_format = Rtl433_output.time_format = Default | ISO | Unix | Unix_usec | Off
55+66+type input_spec = File of string | Rtl_tcp of string * int
77+88+type output_spec =
99+ | Mqtt of Rtl433_output_mqtt.config
1010+ | Json_stdout
1111+ | Json_file of string
1212+ | Log
1313+1414+type t = {
1515+ input : input_spec option;
1616+ outputs : output_spec list;
1717+ frequencies : int list;
1818+ sample_rate : int;
1919+ gain : float option;
2020+ protocol_filter : Rtl433_registry.filter;
2121+ flex_decoders : Rtl433_flex.spec list;
2222+ conversion : conversion;
2323+ time_format : time_format;
2424+ report_protocol : bool;
2525+ hop_interval : float option;
2626+ duration : float option;
2727+ verbosity : int;
2828+}
2929+3030+let default =
3131+ {
3232+ input = None;
3333+ outputs = [ Json_stdout ];
3434+ frequencies = [ 433920000 ];
3535+ sample_rate = 250000;
3636+ gain = None;
3737+ protocol_filter = Rtl433_registry.All;
3838+ flex_decoders = [];
3939+ conversion = Native;
4040+ time_format = Default;
4141+ report_protocol = false;
4242+ hop_interval = None;
4343+ duration = None;
4444+ verbosity = 0;
4545+ }
4646+4747+let parse_frequency str =
4848+ let str = String.trim str in
4949+ let len = String.length str in
5050+ if len = 0 then Error "Empty frequency"
5151+ else
5252+ let last = Char.lowercase_ascii str.[len - 1] in
5353+ try
5454+ match last with
5555+ | 'm' ->
5656+ let num = String.sub str 0 (len - 1) in
5757+ Ok (int_of_float (Float.of_string num *. 1_000_000.0))
5858+ | 'k' ->
5959+ let num = String.sub str 0 (len - 1) in
6060+ Ok (int_of_string num * 1000)
6161+ | '0' .. '9' -> Ok (int_of_string str)
6262+ | _ -> Error ("Invalid frequency suffix: " ^ String.make 1 last)
6363+ with _ -> Error ("Cannot parse frequency: " ^ str)
6464+6565+let parse_line config line =
6666+ let line = String.trim line in
6767+ if String.length line = 0 || line.[0] = '#' then Ok config
6868+ else
6969+ let parts = String.split_on_char ' ' line in
7070+ match parts with
7171+ | "frequency" :: rest | "f" :: rest -> (
7272+ let freq_str = String.concat " " rest in
7373+ match parse_frequency freq_str with
7474+ | Ok freq -> Ok { config with frequencies = freq :: config.frequencies }
7575+ | Error e -> Error e)
7676+ | "protocol" :: rest | "R" :: rest -> (
7777+ match rest with
7878+ | [ num ] -> (
7979+ match int_of_string_opt num with
8080+ | Some n ->
8181+ let filter =
8282+ match config.protocol_filter with
8383+ | Rtl433_registry.All ->
8484+ if n < 0 then Rtl433_registry.Disable [ -n ]
8585+ else Rtl433_registry.Enable [ n ]
8686+ | Rtl433_registry.Enable ns ->
8787+ if n < 0 then Rtl433_registry.Enable ns
8888+ else Rtl433_registry.Enable (n :: ns)
8989+ | Rtl433_registry.Disable ns ->
9090+ if n < 0 then Rtl433_registry.Disable (-n :: ns)
9191+ else Rtl433_registry.Disable ns
9292+ in
9393+ Ok { config with protocol_filter = filter }
9494+ | None -> Error ("Invalid protocol number: " ^ List.hd rest))
9595+ | _ -> Error "protocol requires a number")
9696+ | "output" :: rest -> (
9797+ let output_str = String.concat " " rest in
9898+ if String.starts_with ~prefix:"mqtt://" output_str then
9999+ match Rtl433_output_mqtt.parse_url output_str with
100100+ | Ok mqtt_config ->
101101+ Ok { config with outputs = Mqtt mqtt_config :: config.outputs }
102102+ | Error e -> Error e
103103+ else if output_str = "json" then
104104+ Ok { config with outputs = Json_stdout :: config.outputs }
105105+ else Error ("Unknown output: " ^ output_str))
106106+ | [ "convert"; "si" ] -> Ok { config with conversion = SI }
107107+ | [ "convert"; "customary" ] -> Ok { config with conversion = Customary }
108108+ | [ "convert"; "native" ] -> Ok { config with conversion = Native }
109109+ | "report_meta" :: _ ->
110110+ (* Parse report_meta time:iso:usec:tz etc *)
111111+ Ok { config with time_format = ISO }
112112+ | _ -> Ok config (* Ignore unknown options *)
113113+114114+let parse_string content =
115115+ let lines = String.split_on_char '\n' content in
116116+ List.fold_left
117117+ (fun config_result line ->
118118+ match config_result with
119119+ | Error e -> Error e
120120+ | Ok config -> parse_line config line)
121121+ (Ok default) lines
122122+123123+let parse_file ~fs path =
124124+ try
125125+ let content = Eio.Path.load Eio.Path.(fs / path) in
126126+ parse_string content
127127+ with exn -> Error (Printexc.to_string exn)
128128+129129+let cmdliner_term =
130130+ let open Cmdliner in
131131+ Term.const default
132132+133133+let merge base overlay =
134134+ {
135135+ input = (match overlay.input with Some _ -> overlay.input | None -> base.input);
136136+ outputs =
137137+ (if overlay.outputs = [ Json_stdout ] then base.outputs else overlay.outputs);
138138+ frequencies =
139139+ (if overlay.frequencies = [ 433920000 ] then base.frequencies
140140+ else overlay.frequencies);
141141+ sample_rate =
142142+ (if overlay.sample_rate = 250000 then base.sample_rate
143143+ else overlay.sample_rate);
144144+ gain = (match overlay.gain with Some _ -> overlay.gain | None -> base.gain);
145145+ protocol_filter =
146146+ (match overlay.protocol_filter with
147147+ | Rtl433_registry.All -> base.protocol_filter
148148+ | f -> f);
149149+ flex_decoders = base.flex_decoders @ overlay.flex_decoders;
150150+ conversion =
151151+ (if overlay.conversion = Native then base.conversion else overlay.conversion);
152152+ time_format =
153153+ (if overlay.time_format = Default then base.time_format
154154+ else overlay.time_format);
155155+ report_protocol = overlay.report_protocol || base.report_protocol;
156156+ hop_interval =
157157+ (match overlay.hop_interval with
158158+ | Some _ -> overlay.hop_interval
159159+ | None -> base.hop_interval);
160160+ duration =
161161+ (match overlay.duration with Some _ -> overlay.duration | None -> base.duration);
162162+ verbosity = max base.verbosity overlay.verbosity;
163163+ }
164164+165165+let pp fmt config =
166166+ Format.fprintf fmt "Config {@.";
167167+ Format.fprintf fmt " frequencies: [%s]@."
168168+ (String.concat ", " (List.map string_of_int config.frequencies));
169169+ Format.fprintf fmt " sample_rate: %d@." config.sample_rate;
170170+ Format.fprintf fmt " outputs: %d@." (List.length config.outputs);
171171+ Format.fprintf fmt "}@."
172172+173173+let to_string config = Format.asprintf "%a" pp config
+145
lib/rtl433_config.mli
···11+(** Configuration for rtl433.
22+33+ This module handles configuration from files and command line,
44+ with syntax compatible with rtl_433. *)
55+66+(** {1 Types} *)
77+88+(** Unit conversion mode. *)
99+type conversion = Rtl433_output.conversion =
1010+ | Native
1111+ | SI
1212+ | Customary
1313+1414+(** Time reporting format. *)
1515+type time_format = Rtl433_output.time_format =
1616+ | Default
1717+ | ISO
1818+ | Unix
1919+ | Unix_usec
2020+ | Off
2121+2222+(** Input source specification. *)
2323+type input_spec =
2424+ | File of string
2525+ (** File path for recorded samples. *)
2626+ | Rtl_tcp of string * int
2727+ (** Host and port for rtl_tcp server. *)
2828+2929+(** Output specification. *)
3030+type output_spec =
3131+ | Mqtt of Rtl433_output_mqtt.config
3232+ (** MQTT output. *)
3333+ | Json_stdout
3434+ (** JSON to stdout. *)
3535+ | Json_file of string
3636+ (** JSON to file. *)
3737+ | Log
3838+ (** Human-readable log output. *)
3939+4040+(** Complete configuration. *)
4141+type t = {
4242+ input : input_spec option;
4343+ (** Input source. *)
4444+ outputs : output_spec list;
4545+ (** Output handlers. *)
4646+ frequencies : int list;
4747+ (** Center frequencies in Hz. *)
4848+ sample_rate : int;
4949+ (** Sample rate in Hz. *)
5050+ gain : float option;
5151+ (** Gain in dB, None for auto. *)
5252+ protocol_filter : Rtl433_registry.filter;
5353+ (** Protocol enable/disable filter. *)
5454+ flex_decoders : Rtl433_flex.spec list;
5555+ (** User-defined flex decoders. *)
5656+ conversion : conversion;
5757+ (** Unit conversion mode. *)
5858+ time_format : time_format;
5959+ (** Time format for output. *)
6060+ report_protocol : bool;
6161+ (** Include protocol number in output. *)
6262+ hop_interval : float option;
6363+ (** Frequency hop interval in seconds. *)
6464+ duration : float option;
6565+ (** Run duration in seconds, None for unlimited. *)
6666+ verbosity : int;
6767+ (** Verbosity level (0=normal, 1=verbose, 2+=debug). *)
6868+}
6969+7070+(** Default configuration. *)
7171+val default : t
7272+7373+(** {1 Parsing} *)
7474+7575+(** Parse a configuration file.
7676+7777+ Format is compatible with rtl_433.conf:
7878+ {v
7979+ output mqtt://host:port,user=x,pass=y,retain=true
8080+ frequency 868M
8181+ protocol 142
8282+ protocol -59
8383+ convert si
8484+ v}
8585+8686+ @param fs Filesystem capability
8787+ @param path Configuration file path
8888+ @return Parsed configuration or error *)
8989+val parse_file :
9090+ fs:_ Eio.Path.t ->
9191+ string ->
9292+ (t, string) result
9393+9494+(** Parse configuration from string.
9595+9696+ @param content Configuration file content
9797+ @return Parsed configuration or error *)
9898+val parse_string : string -> (t, string) result
9999+100100+(** Parse a single configuration line.
101101+102102+ @param config Current configuration
103103+ @param line Line to parse
104104+ @return Updated configuration or error *)
105105+val parse_line : t -> string -> (t, string) result
106106+107107+(** {1 Command Line} *)
108108+109109+(** Cmdliner term for configuration.
110110+111111+ @return Term that produces configuration *)
112112+val cmdliner_term : t Cmdliner.Term.t
113113+114114+(** {1 Frequency Parsing} *)
115115+116116+(** Parse frequency string.
117117+118118+ Accepts formats like:
119119+ - "433920000" (Hz)
120120+ - "433.92M" (MHz)
121121+ - "868M" (MHz)
122122+ - "915000k" (kHz)
123123+124124+ @param str Frequency string
125125+ @return Frequency in Hz or error *)
126126+val parse_frequency : string -> (int, string) result
127127+128128+(** {1 Merging} *)
129129+130130+(** Merge two configurations.
131131+132132+ The second configuration's non-default values override the first.
133133+134134+ @param base Base configuration
135135+ @param overlay Overlay configuration
136136+ @return Merged configuration *)
137137+val merge : t -> t -> t
138138+139139+(** {1 Printing} *)
140140+141141+(** Pretty-print configuration. *)
142142+val pp : Format.formatter -> t -> unit
143143+144144+(** Convert configuration to config file format. *)
145145+val to_string : t -> string
+109
lib/rtl433_data.ml
···11+(* Hierarchical data structure *)
22+33+type value =
44+ | Int of int
55+ | Float of float
66+ | String of string
77+ | Data of t
88+ | Array of value array
99+1010+and field = {
1111+ key : string;
1212+ pretty_key : string option;
1313+ format : string option;
1414+ value : value;
1515+}
1616+1717+and t = field list
1818+1919+let make items =
2020+ List.map
2121+ (fun (key, pretty_key, value) -> { key; pretty_key; format = None; value })
2222+ items
2323+2424+let int key ?pretty ?format value =
2525+ { key; pretty_key = pretty; format; value = Int value }
2626+2727+let float key ?pretty ?format value =
2828+ { key; pretty_key = pretty; format; value = Float value }
2929+3030+let string key ?pretty ?format value =
3131+ { key; pretty_key = pretty; format; value = String value }
3232+3333+let data key ?pretty value =
3434+ { key; pretty_key = pretty; format = None; value = Data value }
3535+3636+let array key ?pretty value =
3737+ { key; pretty_key = pretty; format = None; value = Array value }
3838+3939+let hex key ?pretty bytes len =
4040+ let s = Rtl433_util.bytes_to_hex bytes len in
4141+ { key; pretty_key = pretty; format = None; value = String s }
4242+4343+let find key data = List.find_opt (fun f -> f.key = key) data
4444+4545+let get_int key data =
4646+ match find key data with Some { value = Int i; _ } -> Some i | _ -> None
4747+4848+let get_float key data =
4949+ match find key data with Some { value = Float f; _ } -> Some f | _ -> None
5050+5151+let get_string key data =
5252+ match find key data with Some { value = String s; _ } -> Some s | _ -> None
5353+5454+let rec pp_value fmt = function
5555+ | Int i -> Format.fprintf fmt "%d" i
5656+ | Float f -> Format.fprintf fmt "%g" f
5757+ | String s -> Format.fprintf fmt "%S" s
5858+ | Data d -> pp fmt d
5959+ | Array a ->
6060+ Format.fprintf fmt "[%a]"
6161+ (Format.pp_print_list ~pp_sep:(fun fmt () -> Format.fprintf fmt ", ") pp_value)
6262+ (Array.to_list a)
6363+6464+and pp fmt data =
6565+ Format.fprintf fmt "{%a}"
6666+ (Format.pp_print_list
6767+ ~pp_sep:(fun fmt () -> Format.fprintf fmt ", ")
6868+ (fun fmt f -> Format.fprintf fmt "%s: %a" f.key pp_value f.value))
6969+ data
7070+7171+let to_json data =
7272+ let buf = Buffer.create 256 in
7373+ let rec emit_value = function
7474+ | Int i -> Buffer.add_string buf (string_of_int i)
7575+ | Float f -> Buffer.add_string buf (string_of_float f)
7676+ | String s ->
7777+ Buffer.add_char buf '"';
7878+ Buffer.add_string buf (String.escaped s);
7979+ Buffer.add_char buf '"'
8080+ | Data d -> emit_data d
8181+ | Array a ->
8282+ Buffer.add_char buf '[';
8383+ Array.iteri
8484+ (fun i v ->
8585+ if i > 0 then Buffer.add_char buf ',';
8686+ emit_value v)
8787+ a;
8888+ Buffer.add_char buf ']'
8989+ and emit_data fields =
9090+ Buffer.add_char buf '{';
9191+ List.iteri
9292+ (fun i f ->
9393+ if i > 0 then Buffer.add_char buf ',';
9494+ Buffer.add_char buf '"';
9595+ Buffer.add_string buf f.key;
9696+ Buffer.add_string buf "\":";
9797+ emit_value f.value)
9898+ fields;
9999+ Buffer.add_char buf '}'
100100+ in
101101+ emit_data data;
102102+ Buffer.contents buf
103103+104104+(* Mutable storage for last decoded output *)
105105+let last_output : t option ref = ref None
106106+107107+let set_last_output data = last_output := Some data
108108+let get_last_output () = !last_output
109109+let clear_last_output () = last_output := None
+125
lib/rtl433_data.mli
···11+(** Hierarchical data structure for decoded sensor messages.
22+33+ This module provides a flexible data structure for representing
44+ decoded sensor data with typed fields. The structure supports
55+ nested data, arrays, and various scalar types, and can be
66+ serialized to JSON or other formats. *)
77+88+(** {1 Data Values} *)
99+1010+(** A data value that can be stored in a field. *)
1111+type value =
1212+ | Int of int (** Integer value *)
1313+ | Float of float (** Floating-point value *)
1414+ | String of string (** String value *)
1515+ | Data of t (** Nested data structure *)
1616+ | Array of value array (** Array of homogeneous values *)
1717+1818+(** A single field in the data structure. *)
1919+and field = {
2020+ key : string; (** Field identifier (e.g., "temperature_C") *)
2121+ pretty_key : string option;(** Human-readable name (e.g., "Temperature") *)
2222+ format : string option; (** Printf-style format string *)
2323+ value : value; (** The field's value *)
2424+}
2525+2626+(** A data structure is a list of fields. *)
2727+and t = field list
2828+2929+(** {1 Construction} *)
3030+3131+(** Create a data structure from a list of (key, pretty_key, value) tuples. *)
3232+val make : (string * string option * value) list -> t
3333+3434+(** Create an integer field.
3535+3636+ @param key Field key
3737+ @param pretty Optional pretty name
3838+ @param format Optional format string (e.g., "%d")
3939+ @param value Integer value *)
4040+val int : string -> ?pretty:string -> ?format:string -> int -> field
4141+4242+(** Create a float field.
4343+4444+ @param key Field key
4545+ @param pretty Optional pretty name
4646+ @param format Optional format string (e.g., "%.1f C")
4747+ @param value Float value *)
4848+val float : string -> ?pretty:string -> ?format:string -> float -> field
4949+5050+(** Create a string field.
5151+5252+ @param key Field key
5353+ @param pretty Optional pretty name
5454+ @param format Optional format string
5555+ @param value String value *)
5656+val string : string -> ?pretty:string -> ?format:string -> string -> field
5757+5858+(** Create a nested data field.
5959+6060+ @param key Field key
6161+ @param pretty Optional pretty name
6262+ @param value Nested data structure *)
6363+val data : string -> ?pretty:string -> t -> field
6464+6565+(** Create an array field.
6666+6767+ @param key Field key
6868+ @param pretty Optional pretty name
6969+ @param value Array of values *)
7070+val array : string -> ?pretty:string -> value array -> field
7171+7272+(** Create a hex string field from bytes.
7373+7474+ @param key Field key
7575+ @param pretty Optional pretty name
7676+ @param bytes Raw bytes
7777+ @param len Number of bytes to include *)
7878+val hex : string -> ?pretty:string -> bytes -> int -> field
7979+8080+(** {1 Field Access} *)
8181+8282+(** Find a field by key.
8383+8484+ @param key Field key to search for
8585+ @param data Data structure to search
8686+ @return Field if found *)
8787+val find : string -> t -> field option
8888+8989+(** Get an integer value by key.
9090+9191+ @param key Field key
9292+ @param data Data structure
9393+ @return Integer value if field exists and is an Int *)
9494+val get_int : string -> t -> int option
9595+9696+(** Get a float value by key. *)
9797+val get_float : string -> t -> float option
9898+9999+(** Get a string value by key. *)
100100+val get_string : string -> t -> string option
101101+102102+(** {1 Serialization} *)
103103+104104+(** Convert data structure to JSON string.
105105+106106+ @param data Data structure
107107+ @return JSON representation *)
108108+val to_json : t -> string
109109+110110+(** Pretty-print data structure. *)
111111+val pp : Format.formatter -> t -> unit
112112+113113+(** Pretty-print a single value. *)
114114+val pp_value : Format.formatter -> value -> unit
115115+116116+(** {1 Decoder Output} *)
117117+118118+(** Set the last decoded output (called by decoders). *)
119119+val set_last_output : t -> unit
120120+121121+(** Get the last decoded output (called by pipeline). *)
122122+val get_last_output : unit -> t option
123123+124124+(** Clear the last decoded output. *)
125125+val clear_last_output : unit -> unit
+174
lib/rtl433_decoder_fineoffset.ml
···11+(* FineOffset WH51 Soil Moisture Sensor decoder
22+33+ Protocol 142 in rtl_433
44+55+ Modulation: FSK_PULSE_PCM
66+ Bit width: 58 µs
77+88+ Packet format:
99+ - Preamble: 0xAA 0x2D 0xD4
1010+ - 14 bytes data
1111+1212+ Data layout:
1313+ - byte 0: Family code 0x51
1414+ - byte 1-3: Device ID (24 bits)
1515+ - byte 4 high nibble: Battery status (0=OK, 8=low)
1616+ - byte 4 low nibble: Transmission counter
1717+ - byte 5: Moisture percentage (0-100)
1818+ - byte 6-7: Raw AD value (big endian)
1919+ - byte 8: Boost/interval
2020+ - byte 9-10: Battery voltage (raw)
2121+ - byte 11: Sequence/flags
2222+ - byte 12: CRC8 (poly 0x31, init 0x00) over bytes 0-11
2323+ - byte 13: Checksum (sum of bytes 0-12 & 0xff)
2424+*)
2525+2626+(* Find the preamble 0xAA 0x2D 0xD4 in the bit buffer *)
2727+let find_preamble bits =
2828+ let num_bits = Rtl433_bitbuffer.bits_per_row bits 0 in
2929+ let preamble = [| 0xAA; 0x2D; 0xD4 |] in
3030+3131+ let rec search bit_offset =
3232+ if bit_offset + (14 * 8) > num_bits then None
3333+ else begin
3434+ let matches = ref true in
3535+ for i = 0 to 2 do
3636+ if !matches then begin
3737+ let byte = Rtl433_bitbuffer.get_byte bits ~row:0 ~bit_offset:(bit_offset + (i * 8)) in
3838+ if byte <> preamble.(i) then matches := false
3939+ end
4040+ done;
4141+ if !matches then Some (bit_offset + 24) (* Skip past preamble *)
4242+ else search (bit_offset + 1)
4343+ end
4444+ in
4545+ search 0
4646+4747+let decode_wh51 () bits =
4848+ let num_bits = Rtl433_bitbuffer.bits_per_row bits 0 in
4949+5050+ (* Need at least preamble (24 bits) + 14 bytes data *)
5151+ if num_bits < 24 + (14 * 8) then
5252+ Rtl433_device.Abort_length
5353+ else
5454+ match find_preamble bits with
5555+ | None -> Rtl433_device.Abort_early
5656+ | Some data_start ->
5757+ (* Extract 14 bytes of data *)
5858+ let data = Bytes.create 14 in
5959+ for i = 0 to 13 do
6060+ let byte = Rtl433_bitbuffer.get_byte bits ~row:0 ~bit_offset:(data_start + (i * 8)) in
6161+ Bytes.set_uint8 data i byte
6262+ done;
6363+6464+ (* Check family code *)
6565+ let family = Bytes.get_uint8 data 0 in
6666+ if family <> 0x51 then
6767+ Rtl433_device.Fail_sanity
6868+ else begin
6969+ (* Verify CRC8 over bytes 0-11 *)
7070+ let crc = Rtl433_util.crc8 ~poly:0x31 ~init:0x00 data 12 in
7171+ let expected_crc = Bytes.get_uint8 data 12 in
7272+ if crc <> expected_crc then
7373+ Rtl433_device.Fail_mic
7474+ else begin
7575+ (* Verify checksum *)
7676+ let sum = Rtl433_util.add_bytes data 13 in
7777+ let expected_sum = Bytes.get_uint8 data 13 in
7878+ if sum <> expected_sum then
7979+ Rtl433_device.Fail_mic
8080+ else begin
8181+ (* Extract fields *)
8282+ let id =
8383+ (Bytes.get_uint8 data 1 lsl 16) lor
8484+ (Bytes.get_uint8 data 2 lsl 8) lor
8585+ (Bytes.get_uint8 data 3)
8686+ in
8787+ let id_str = Printf.sprintf "%06x" id in
8888+8989+ let battery_status = (Bytes.get_uint8 data 4 lsr 4) land 0x0f in
9090+ let battery_ok = if battery_status = 0 then 1.0 else 0.0 in
9191+9292+ let counter = Bytes.get_uint8 data 4 land 0x0f in
9393+ let moisture = Bytes.get_uint8 data 5 in
9494+9595+ let ad_raw =
9696+ (Bytes.get_uint8 data 6 lsl 8) lor
9797+ (Bytes.get_uint8 data 7)
9898+ in
9999+100100+ let boost = Bytes.get_uint8 data 8 in
101101+102102+ let battery_raw =
103103+ (Bytes.get_uint8 data 9 lsl 8) lor
104104+ (Bytes.get_uint8 data 10)
105105+ in
106106+ (* Battery voltage: raw * 2 mV according to analysis *)
107107+ let battery_mv = battery_raw * 2 in
108108+109109+ (* Output data *)
110110+ let output_data = Rtl433_data.make [
111111+ ("model", None, Rtl433_data.String "Fineoffset-WH51");
112112+ ("id", None, Rtl433_data.String id_str);
113113+ ("battery_ok", None, Rtl433_data.Float battery_ok);
114114+ ("battery_mV", Some "mV", Rtl433_data.Int battery_mv);
115115+ ("moisture", Some "%", Rtl433_data.Int moisture);
116116+ ("boost", None, Rtl433_data.Int boost);
117117+ ("ad_raw", None, Rtl433_data.Int ad_raw);
118118+ ("counter", None, Rtl433_data.Int counter);
119119+ ("mic", None, Rtl433_data.String "CRC");
120120+ ] in
121121+122122+ (* Store output data for later retrieval *)
123123+ Rtl433_data.set_last_output output_data;
124124+125125+ Rtl433_device.Decoded 1
126126+ end
127127+ end
128128+ end
129129+130130+let create_wh51 () =
131131+ let timing = Rtl433_device.make_timing
132132+ ~short_width:58.0 (* 58 µs bit width *)
133133+ ~long_width:58.0
134134+ ~reset_limit:10000.0 (* 10 ms reset *)
135135+ ~tolerance:10.0
136136+ ()
137137+ in
138138+ Rtl433_device.create
139139+ ~name:"Fineoffset-WH51"
140140+ ~protocol_num:142
141141+ ~modulation:(Rtl433_modulation.FSK Rtl433_modulation.Pulse_pcm)
142142+ ~timing
143143+ ~decode:decode_wh51
144144+ ~fields:["model"; "id"; "battery_ok"; "battery_mV"; "moisture"; "boost"; "ad_raw"; "counter"; "mic"]
145145+ ()
146146+147147+(* Additional FineOffset decoders can be added here *)
148148+149149+(* WH2 Temperature/Humidity sensor - protocol 32 *)
150150+let decode_wh2 () _bits =
151151+ (* TODO: implement *)
152152+ Rtl433_device.Abort_early
153153+154154+let create_wh2 () =
155155+ let timing = Rtl433_device.make_timing
156156+ ~short_width:500.0
157157+ ~long_width:1500.0
158158+ ~reset_limit:9000.0
159159+ ()
160160+ in
161161+ Rtl433_device.create
162162+ ~name:"Fineoffset-WH2"
163163+ ~protocol_num:32
164164+ ~modulation:(Rtl433_modulation.OOK Rtl433_modulation.Pulse_pwm)
165165+ ~timing
166166+ ~decode:decode_wh2
167167+ ~fields:["model"; "id"; "temperature_C"; "humidity"]
168168+ ()
169169+170170+(* Return all FineOffset decoders *)
171171+let all_decoders () = [
172172+ create_wh51 ();
173173+ create_wh2 ();
174174+]
···11+(** Flexible decoder system for user-defined protocols.
22+33+ The flex decoder allows defining custom protocol decoders via
44+ configuration rather than code. This enables adding support for
55+ new sensors without modifying the library. *)
66+77+(** {1 Flex Decoder Specification} *)
88+99+(** Bit encoding type for the flex decoder. *)
1010+type encoding =
1111+ | PCM
1212+ (** Pulse Code Modulation (NRZ): pulse=1, no pulse=0. *)
1313+ | PWM
1414+ (** Pulse Width Modulation: short=1, long=0. *)
1515+ | PPM
1616+ (** Pulse Position Modulation: short gap=0, long gap=1. *)
1717+ | Manchester
1818+ (** Manchester encoding: transition direction determines bit. *)
1919+ | Differential_manchester
2020+ (** Differential Manchester: transition presence determines bit. *)
2121+2222+(** Specification for a flex decoder. *)
2323+type spec = {
2424+ name : string;
2525+ (** Protocol name. *)
2626+ modulation : Rtl433_modulation.t;
2727+ (** RF modulation type. *)
2828+ short_width : float;
2929+ (** Short pulse/gap width in microseconds. *)
3030+ long_width : float;
3131+ (** Long pulse/gap width in microseconds. *)
3232+ reset_limit : float;
3333+ (** End of transmission gap limit in microseconds. *)
3434+ gap_limit : float option;
3535+ (** Maximum gap within transmission (us). *)
3636+ sync_width : float option;
3737+ (** Sync pulse width in microseconds. *)
3838+ tolerance : float;
3939+ (** Timing tolerance in microseconds. *)
4040+ preamble : bytes option;
4141+ (** Expected preamble bytes (for validation). *)
4242+ encoding : encoding;
4343+ (** Bit encoding type. *)
4444+ min_bits : int;
4545+ (** Minimum valid message length in bits. *)
4646+ max_bits : int option;
4747+ (** Maximum valid message length in bits. *)
4848+ match_len : int option;
4949+ (** Expected exact message length in bits. *)
5050+ count_only : bool;
5151+ (** If true, just count bits without further processing. *)
5252+ invert : bool;
5353+ (** If true, invert all bits before processing. *)
5454+ reflect_bytes : bool;
5555+ (** If true, reverse bit order within each byte. *)
5656+ unique : bool;
5757+ (** If true, deduplicate repeated rows. *)
5858+ fields : string list;
5959+ (** Output field names. *)
6060+}
6161+6262+(** {1 Construction} *)
6363+6464+(** Create a flex decoder from a specification.
6565+6666+ @param spec Flex decoder specification
6767+ @return A decoder that can be registered *)
6868+val create : spec -> unit Rtl433_device.t
6969+7070+(** Default flex specification with common values. *)
7171+val default_spec : spec
7272+7373+(** {1 Parsing} *)
7474+7575+(** Parse a flex decoder specification from a string.
7676+7777+ Format matches rtl_433's -X option:
7878+ {v
7979+ n=name,m=OOK_PWM,s=400,l=800,r=4000,g=1000,t=100
8080+ v}
8181+8282+ Keys:
8383+ - n: name
8484+ - m: modulation (OOK_PCM, OOK_PWM, OOK_PPM, OOK_MC, FSK_PCM, etc.)
8585+ - s: short width (us)
8686+ - l: long width (us)
8787+ - r: reset limit (us)
8888+ - g: gap limit (us)
8989+ - t: tolerance (us)
9090+ - preamble: hex bytes
9191+ - bits: expected bit count
9292+ - unique: deduplicate rows
9393+9494+ @param str Specification string
9595+ @return Parsed spec, or Error message *)
9696+val parse : string -> (spec, string) result
9797+9898+(** {1 Printing} *)
9999+100100+(** Pretty-print a flex specification. *)
101101+val pp_spec : Format.formatter -> spec -> unit
102102+103103+(** Convert spec to rtl_433-compatible string. *)
104104+val spec_to_string : spec -> string
+136
lib/rtl433_input.ml
···11+(* Input sources *)
22+33+type format = Rtl433_baseband.sample_format = CU8 | CS8 | CS16 | CF32
44+55+(* Use an opaque type for the socket to avoid type variance issues *)
66+type socket
77+88+let socket_of_flow (flow : 'a) : socket = Obj.magic flow
99+let flow_of_socket (sock : socket) : Eio.Flow.two_way_ty Eio.Resource.t = Obj.magic sock
1010+1111+type source =
1212+ | File_source of {
1313+ flow : Eio.File.ro_ty Eio.Resource.t;
1414+ format : format;
1515+ sample_rate : int;
1616+ }
1717+ | Rtl_tcp_source of {
1818+ flow : socket;
1919+ mutable frequency : int;
2020+ mutable sample_rate : int;
2121+ mutable gain : int;
2222+ }
2323+2424+type t = { source : source }
2525+2626+let open_file ~sw ~fs ~path ~format ~sample_rate =
2727+ let full_path = Eio.Path.(fs / path) in
2828+ let flow = Eio.Path.open_in ~sw full_path in
2929+ { source = File_source { flow; format; sample_rate } }
3030+3131+let detect_format filename =
3232+ let filename = String.lowercase_ascii filename in
3333+ if String.ends_with ~suffix:".cu8" filename then Some CU8
3434+ else if String.ends_with ~suffix:".cs8" filename then Some CS8
3535+ else if String.ends_with ~suffix:".cs16" filename then Some CS16
3636+ else if String.ends_with ~suffix:".cf32" filename then Some CF32
3737+ else None
3838+3939+let detect_sample_rate filename =
4040+ (* Look for patterns like 250k, 1M, 2.4M *)
4141+ let re_k = Str.regexp "\\([0-9]+\\)k" in
4242+ let re_m = Str.regexp "\\([0-9.]+\\)[mM]" in
4343+ try
4444+ if Str.search_forward re_m filename 0 >= 0 then
4545+ let m = Str.matched_group 1 filename in
4646+ Some (int_of_float (Float.of_string m *. 1_000_000.0))
4747+ else if Str.search_forward re_k filename 0 >= 0 then
4848+ let k = Str.matched_group 1 filename in
4949+ Some (int_of_string k * 1000)
5050+ else None
5151+ with Not_found -> None
5252+5353+let connect_rtl_tcp ~sw ~net ~host ?(port = 1234) () =
5454+ let addr = `Tcp (Eio.Net.Ipaddr.of_raw (Unix.inet_addr_of_string host |> Obj.magic), port) in
5555+ let flow = Eio.Net.connect ~sw net addr in
5656+ {
5757+ source =
5858+ Rtl_tcp_source { flow = socket_of_flow flow; frequency = 433920000; sample_rate = 250000; gain = 0 };
5959+ }
6060+6161+let send_rtl_tcp_cmd flow cmd param =
6262+ let buf = Bytes.create 5 in
6363+ Bytes.set_uint8 buf 0 cmd;
6464+ Bytes.set_int32_be buf 1 (Int32.of_int param);
6565+ Eio.Flow.write flow [ Cstruct.of_bytes buf ]
6666+6767+let set_frequency t freq =
6868+ match t.source with
6969+ | Rtl_tcp_source s ->
7070+ send_rtl_tcp_cmd (flow_of_socket s.flow) 0x01 freq;
7171+ s.frequency <- freq
7272+ | File_source _ -> ()
7373+7474+let set_sample_rate t rate =
7575+ match t.source with
7676+ | Rtl_tcp_source s ->
7777+ send_rtl_tcp_cmd (flow_of_socket s.flow) 0x02 rate;
7878+ s.sample_rate <- rate
7979+ | File_source _ -> ()
8080+8181+let set_gain t gain =
8282+ match t.source with
8383+ | Rtl_tcp_source s ->
8484+ send_rtl_tcp_cmd (flow_of_socket s.flow) 0x04 gain;
8585+ s.gain <- gain
8686+ | File_source _ -> ()
8787+8888+let set_agc t enable =
8989+ match t.source with
9090+ | Rtl_tcp_source s -> send_rtl_tcp_cmd (flow_of_socket s.flow) 0x08 (if enable then 1 else 0)
9191+ | File_source _ -> ()
9292+9393+let read t buf =
9494+ match t.source with
9595+ | File_source { flow; _ } ->
9696+ Eio.Flow.single_read flow (Cstruct.of_bytes buf)
9797+ | Rtl_tcp_source { flow; _ } ->
9898+ Eio.Flow.single_read (flow_of_socket flow) (Cstruct.of_bytes buf)
9999+100100+let read_timeout t buf ~clock ~timeout_ms =
101101+ try
102102+ Eio.Time.with_timeout_exn clock (timeout_ms /. 1000.0) (fun () -> read t buf)
103103+ with Eio.Time.Timeout -> 0
104104+105105+let sample_rate t =
106106+ match t.source with
107107+ | File_source { sample_rate; _ } -> sample_rate
108108+ | Rtl_tcp_source { sample_rate; _ } -> sample_rate
109109+110110+let format t =
111111+ match t.source with
112112+ | File_source { format; _ } -> format
113113+ | Rtl_tcp_source _ -> CU8 (* rtl_tcp always uses CU8 *)
114114+115115+let frequency t =
116116+ match t.source with
117117+ | File_source _ -> 0
118118+ | Rtl_tcp_source { frequency; _ } -> frequency
119119+120120+let is_network t =
121121+ match t.source with File_source _ -> false | Rtl_tcp_source _ -> true
122122+123123+let close _t = () (* Resources cleaned up by Eio switch *)
124124+125125+let format_to_string = function
126126+ | CU8 -> "CU8"
127127+ | CS8 -> "CS8"
128128+ | CS16 -> "CS16"
129129+ | CF32 -> "CF32"
130130+131131+let pp fmt t =
132132+ match t.source with
133133+ | File_source { format; sample_rate; _ } ->
134134+ Format.fprintf fmt "File<%s, %d Hz>" (format_to_string format) sample_rate
135135+ | Rtl_tcp_source { frequency; sample_rate; _ } ->
136136+ Format.fprintf fmt "rtl_tcp<%d Hz, %d sps>" frequency sample_rate
+168
lib/rtl433_input.mli
···11+(** Input sources for I/Q sample data.
22+33+ This module provides an abstraction for different input sources
44+ including files and rtl_tcp network connections. *)
55+66+(** {1 Types} *)
77+88+(** Sample format. *)
99+type format = Rtl433_baseband.sample_format =
1010+ | CU8 (** Complex unsigned 8-bit *)
1111+ | CS8 (** Complex signed 8-bit *)
1212+ | CS16 (** Complex signed 16-bit *)
1313+ | CF32 (** Complex float 32-bit *)
1414+1515+(** Input source handle. *)
1616+type t
1717+1818+(** {1 File Input} *)
1919+2020+(** Open a file as an input source.
2121+2222+ @param sw Eio switch for resource management
2323+ @param fs Filesystem capability
2424+ @param path File path
2525+ @param format Sample format
2626+ @param sample_rate Sample rate in Hz
2727+ @return Input source *)
2828+val open_file :
2929+ sw:Eio.Switch.t ->
3030+ fs:_ Eio.Path.t ->
3131+ path:string ->
3232+ format:format ->
3333+ sample_rate:int ->
3434+ t
3535+3636+(** Detect format from filename.
3737+3838+ Examines file extension to determine format:
3939+ - .cu8 -> CU8
4040+ - .cs8 -> CS8
4141+ - .cs16 -> CS16
4242+ - .cf32 -> CF32
4343+4444+ @param filename Filename to examine
4545+ @return Detected format or None *)
4646+val detect_format : string -> format option
4747+4848+(** Detect sample rate from filename.
4949+5050+ Looks for patterns like "250k" or "1M" in filename.
5151+5252+ @param filename Filename to examine
5353+ @return Detected sample rate or None *)
5454+val detect_sample_rate : string -> int option
5555+5656+(** {1 RTL-TCP Input} *)
5757+5858+(** Connect to an rtl_tcp server.
5959+6060+ rtl_tcp is a server that streams I/Q samples over TCP.
6161+ This allows using SDR devices on remote machines.
6262+6363+ @param sw Eio switch for resource management
6464+ @param net Network capability
6565+ @param host Server hostname
6666+ @param port Server port (default 1234)
6767+ @return Input source *)
6868+val connect_rtl_tcp :
6969+ sw:Eio.Switch.t ->
7070+ net:_ Eio.Net.t ->
7171+ host:string ->
7272+ ?port:int ->
7373+ unit ->
7474+ t
7575+7676+(** {1 RTL-TCP Commands} *)
7777+7878+(** Set center frequency on rtl_tcp source.
7979+8080+ @param input Input source (must be rtl_tcp)
8181+ @param freq Frequency in Hz *)
8282+val set_frequency : t -> int -> unit
8383+8484+(** Set sample rate on rtl_tcp source.
8585+8686+ @param input Input source
8787+ @param rate Sample rate in Hz *)
8888+val set_sample_rate : t -> int -> unit
8989+9090+(** Set gain on rtl_tcp source.
9191+9292+ @param input Input source
9393+ @param gain Gain in tenths of dB (e.g., 400 = 40.0 dB).
9494+ Use 0 for automatic gain. *)
9595+val set_gain : t -> int -> unit
9696+9797+(** Enable/disable automatic gain control.
9898+9999+ @param input Input source
100100+ @param enable True for AGC, false for manual gain *)
101101+val set_agc : t -> bool -> unit
102102+103103+(** {1 Reading Samples} *)
104104+105105+(** Read samples from the input source.
106106+107107+ @param input Input source
108108+ @param buf Buffer to read into
109109+ @return Number of bytes read, 0 for EOF *)
110110+val read : t -> bytes -> int
111111+112112+(** Read samples with timeout.
113113+114114+ @param input Input source
115115+ @param buf Buffer to read into
116116+ @param clock Eio clock capability
117117+ @param timeout_ms Timeout in milliseconds
118118+ @return Number of bytes read, 0 for timeout/EOF *)
119119+val read_timeout :
120120+ t ->
121121+ bytes ->
122122+ clock:_ Eio.Time.clock ->
123123+ timeout_ms:float ->
124124+ int
125125+126126+(** {1 Properties} *)
127127+128128+(** Get the sample rate.
129129+130130+ @param input Input source
131131+ @return Sample rate in Hz *)
132132+val sample_rate : t -> int
133133+134134+(** Get the sample format.
135135+136136+ @param input Input source
137137+ @return Sample format *)
138138+val format : t -> format
139139+140140+(** Get the current center frequency.
141141+142142+ @param input Input source
143143+ @return Center frequency in Hz, or 0 if unknown *)
144144+val frequency : t -> int
145145+146146+(** Check if input source is from a network connection.
147147+148148+ @param input Input source
149149+ @return True for rtl_tcp, false for file *)
150150+val is_network : t -> bool
151151+152152+(** {1 Lifecycle} *)
153153+154154+(** Close the input source.
155155+156156+ For file inputs, closes the file.
157157+ For rtl_tcp, disconnects from the server.
158158+159159+ Note: Also closed automatically when the Eio switch completes. *)
160160+val close : t -> unit
161161+162162+(** {1 Printing} *)
163163+164164+(** Pretty-print input source info. *)
165165+val pp : Format.formatter -> t -> unit
166166+167167+(** Format string for sample format. *)
168168+val format_to_string : format -> string
+33
lib/rtl433_modulation.ml
···11+(* Modulation types *)
22+33+type ook =
44+ | Pulse_pcm
55+ | Pulse_ppm
66+ | Pulse_pwm
77+ | Pulse_manchester
88+ | Pulse_dmc
99+ | Pulse_piwm_raw
1010+ | Pulse_piwm_dc
1111+ | Pulse_nrzs
1212+1313+type fsk = Pulse_pcm | Pulse_pwm | Pulse_manchester
1414+1515+type t = OOK of ook | FSK of fsk
1616+1717+let pp fmt t =
1818+ match t with
1919+ | OOK Pulse_pcm -> Format.fprintf fmt "OOK_PULSE_PCM"
2020+ | OOK Pulse_ppm -> Format.fprintf fmt "OOK_PULSE_PPM"
2121+ | OOK Pulse_pwm -> Format.fprintf fmt "OOK_PULSE_PWM"
2222+ | OOK Pulse_manchester -> Format.fprintf fmt "OOK_PULSE_MANCHESTER"
2323+ | OOK Pulse_dmc -> Format.fprintf fmt "OOK_PULSE_DMC"
2424+ | OOK Pulse_piwm_raw -> Format.fprintf fmt "OOK_PULSE_PIWM_RAW"
2525+ | OOK Pulse_piwm_dc -> Format.fprintf fmt "OOK_PULSE_PIWM_DC"
2626+ | OOK Pulse_nrzs -> Format.fprintf fmt "OOK_PULSE_NRZS"
2727+ | FSK Pulse_pcm -> Format.fprintf fmt "FSK_PULSE_PCM"
2828+ | FSK Pulse_pwm -> Format.fprintf fmt "FSK_PULSE_PWM"
2929+ | FSK Pulse_manchester -> Format.fprintf fmt "FSK_PULSE_MANCHESTER"
3030+3131+let to_string t = Format.asprintf "%a" pp t
3232+let is_fsk = function FSK _ -> true | OOK _ -> false
3333+let is_ook = function OOK _ -> true | FSK _ -> false
+65
lib/rtl433_modulation.mli
···11+(** Modulation and coding types for RF signals.
22+33+ This module defines the modulation schemes used by wireless sensors.
44+ The term "modulation" here refers to the combination of:
55+ - RF modulation (OOK or FSK onto the carrier)
66+ - Line coding (how bits are encoded as pulses/gaps) *)
77+88+(** {1 OOK Modulation Types}
99+1010+ On-Off Keying modulates a carrier by turning it on and off.
1111+ Different line codings determine how bits map to on/off states. *)
1212+1313+type ook =
1414+ | Pulse_pcm
1515+ (** Non-Return-to-Zero: Pulse = 1, No pulse = 0.
1616+ Also known as RZ (Return-to-Zero) coding. *)
1717+ | Pulse_ppm
1818+ (** Pulse Position Modulation: Short gap = 0, Long gap = 1. *)
1919+ | Pulse_pwm
2020+ (** Pulse Width Modulation: Short pulse = 1, Long pulse = 0. *)
2121+ | Pulse_manchester
2222+ (** Manchester with leading zero bit.
2323+ Rising edge = 0, Falling edge = 1. *)
2424+ | Pulse_dmc
2525+ (** Differential Manchester Coding.
2626+ Level shift within clock cycle. *)
2727+ | Pulse_piwm_raw
2828+ (** Pulse Interval/Width Modulation (raw). *)
2929+ | Pulse_piwm_dc
3030+ (** Pulse Interval/Width Modulation (DC balanced). *)
3131+ | Pulse_nrzs
3232+ (** Non-Return-to-Zero Space coding. *)
3333+3434+(** {1 FSK Modulation Types}
3535+3636+ Frequency Shift Keying modulates a carrier by shifting its frequency.
3737+ F1 and F2 frequencies represent the two symbol states. *)
3838+3939+type fsk =
4040+ | Pulse_pcm
4141+ (** FSK with NRZ coding: F1 = 1, F2 = 0. *)
4242+ | Pulse_pwm
4343+ (** FSK with Pulse Width coding. *)
4444+ | Pulse_manchester
4545+ (** FSK with Manchester coding. *)
4646+4747+(** {1 Combined Modulation Type} *)
4848+4949+type t =
5050+ | OOK of ook (** On-Off Keying modulation *)
5151+ | FSK of fsk (** Frequency Shift Keying modulation *)
5252+5353+(** {1 Functions} *)
5454+5555+(** Pretty-print modulation type. *)
5656+val pp : Format.formatter -> t -> unit
5757+5858+(** Convert modulation to string. *)
5959+val to_string : t -> string
6060+6161+(** Check if modulation is FSK-based. *)
6262+val is_fsk : t -> bool
6363+6464+(** Check if modulation is OOK-based. *)
6565+val is_ook : t -> bool
+98
lib/rtl433_output.ml
···11+(* Output handlers *)
22+33+type file_sink = {
44+ mutable buf : Buffer.t;
55+ flow : Eio.File.rw_ty Eio.Resource.t;
66+}
77+88+type handler =
99+ | Json_stdout
1010+ | Json_file of file_sink
1111+ | Log_handler
1212+ | Null_handler
1313+ | Custom_handler of (Rtl433_data.t -> unit)
1414+ | Combined of handler list
1515+1616+type t = { handler : handler }
1717+1818+let json_stdout () = { handler = Json_stdout }
1919+2020+let json_file ~sw ~fs ~path ?append:_ () =
2121+ let full_path = Eio.Path.(fs / path) in
2222+ let flow = Eio.Path.open_out ~sw ~create:(`If_missing 0o644) full_path in
2323+ let buf = Buffer.create 4096 in
2424+ { handler = Json_file { buf; flow } }
2525+2626+let log_output () = { handler = Log_handler }
2727+let null () = { handler = Null_handler }
2828+let custom fn = { handler = Custom_handler fn }
2929+3030+let combine handlers =
3131+ { handler = Combined (List.map (fun t -> t.handler) handlers) }
3232+3333+let rec output_handler handler data =
3434+ match handler with
3535+ | Json_stdout ->
3636+ let json = Rtl433_data.to_json data in
3737+ print_endline json
3838+ | Json_file sink ->
3939+ let json = Rtl433_data.to_json data in
4040+ Buffer.add_string sink.buf json;
4141+ Buffer.add_char sink.buf '\n';
4242+ if Buffer.length sink.buf > 4096 then begin
4343+ Eio.Flow.write sink.flow [ Cstruct.of_string (Buffer.contents sink.buf) ];
4444+ Buffer.clear sink.buf
4545+ end
4646+ | Log_handler -> Format.printf "%a@." Rtl433_data.pp data
4747+ | Null_handler -> ()
4848+ | Custom_handler fn -> fn data
4949+ | Combined handlers -> List.iter (fun h -> output_handler h data) handlers
5050+5151+let output t data = output_handler t.handler data
5252+5353+let rec flush_handler handler =
5454+ match handler with
5555+ | Json_file sink ->
5656+ if Buffer.length sink.buf > 0 then begin
5757+ Eio.Flow.write sink.flow [ Cstruct.of_string (Buffer.contents sink.buf) ];
5858+ Buffer.clear sink.buf
5959+ end
6060+ | Combined handlers -> List.iter flush_handler handlers
6161+ | _ -> ()
6262+6363+let flush t = flush_handler t.handler
6464+let close _t = ()
6565+6666+type conversion = Native | SI | Customary
6767+type time_format = Default | ISO | Unix | Unix_usec | Off
6868+6969+let convert_units conversion data =
7070+ match conversion with
7171+ | Native -> data
7272+ | SI -> data (* TODO: implement conversions *)
7373+ | Customary -> data (* TODO: implement conversions *)
7474+7575+let add_metadata ?time_format ?include_protocol data =
7676+ let time_field =
7777+ match time_format with
7878+ | Some Off | None -> []
7979+ | Some (Default | ISO) ->
8080+ let now = Ptime_clock.now () in
8181+ [ Rtl433_data.string "time" (Ptime.to_rfc3339 now) ]
8282+ | Some Unix ->
8383+ let now = Ptime_clock.now () in
8484+ let ts = Ptime.to_float_s now in
8585+ [ Rtl433_data.float "time" ts ]
8686+ | Some Unix_usec ->
8787+ let now = Ptime_clock.now () in
8888+ let ts = Ptime.to_float_s now in
8989+ [ Rtl433_data.float "time" (ts *. 1_000_000.0) ]
9090+ in
9191+ let protocol_field =
9292+ match include_protocol with
9393+ | Some true -> [ Rtl433_data.int "protocol" 0 ]
9494+ | _ -> []
9595+ in
9696+ time_field @ protocol_field @ data
9797+9898+let pp fmt _t = Format.fprintf fmt "Output"
+118
lib/rtl433_output.mli
···11+(** Output handlers for decoded sensor data.
22+33+ This module provides an abstraction for outputting decoded data
44+ to various destinations (JSON, log, file, etc.). *)
55+66+(** {1 Output Handler Interface} *)
77+88+(** An output handler. *)
99+type t
1010+1111+(** {1 Construction} *)
1212+1313+(** Create a JSON output handler to stdout.
1414+1515+ Outputs each decoded message as a single-line JSON object. *)
1616+val json_stdout : unit -> t
1717+1818+(** Create a JSON output handler to a file.
1919+2020+ @param sw Eio switch for resource management
2121+ @param fs Filesystem capability
2222+ @param path Output file path
2323+ @param append If true, append to existing file *)
2424+val json_file :
2525+ sw:Eio.Switch.t ->
2626+ fs:_ Eio.Path.t ->
2727+ path:string ->
2828+ ?append:bool ->
2929+ unit ->
3030+ t
3131+3232+(** Create a log-style output handler.
3333+3434+ Outputs human-readable formatted messages. *)
3535+val log_output : unit -> t
3636+3737+(** Create a null output handler (discards all output). *)
3838+val null : unit -> t
3939+4040+(** Create a custom output handler.
4141+4242+ @param output_fn Function called for each decoded message *)
4343+val custom : (Rtl433_data.t -> unit) -> t
4444+4545+(** {1 Multiple Outputs} *)
4646+4747+(** Combine multiple output handlers.
4848+4949+ Data is sent to all handlers. *)
5050+val combine : t list -> t
5151+5252+(** {1 Using Outputs} *)
5353+5454+(** Output a decoded message.
5555+5656+ @param handler Output handler
5757+ @param data Decoded data to output *)
5858+val output : t -> Rtl433_data.t -> unit
5959+6060+(** Flush any buffered output.
6161+6262+ @param handler Output handler *)
6363+val flush : t -> unit
6464+6565+(** Close the output handler.
6666+6767+ @param handler Output handler *)
6868+val close : t -> unit
6969+7070+(** {1 Conversion Modes} *)
7171+7272+(** Unit conversion mode for output. *)
7373+type conversion =
7474+ | Native
7575+ (** Use native units from decoder (may vary). *)
7676+ | SI
7777+ (** Convert to SI units (Celsius, m/s, hPa, mm). *)
7878+ | Customary
7979+ (** Convert to US customary units (Fahrenheit, mph, inHg, in). *)
8080+8181+(** Apply unit conversion to data.
8282+8383+ @param conversion Conversion mode
8484+ @param data Input data
8585+ @return Data with converted values *)
8686+val convert_units : conversion -> Rtl433_data.t -> Rtl433_data.t
8787+8888+(** {1 Metadata} *)
8989+9090+(** Time format for metadata. *)
9191+type time_format =
9292+ | Default
9393+ (** Default format from ptime *)
9494+ | ISO
9595+ (** ISO 8601 format *)
9696+ | Unix
9797+ (** Unix timestamp *)
9898+ | Unix_usec
9999+ (** Unix timestamp with microseconds *)
100100+ | Off
101101+ (** No time metadata *)
102102+103103+(** Add metadata to decoded data.
104104+105105+ @param time_format Time format to use
106106+ @param include_protocol Include protocol number
107107+ @param data Input data
108108+ @return Data with metadata added *)
109109+val add_metadata :
110110+ ?time_format:time_format ->
111111+ ?include_protocol:bool ->
112112+ Rtl433_data.t ->
113113+ Rtl433_data.t
114114+115115+(** {1 Printing} *)
116116+117117+(** Pretty-print output handler info. *)
118118+val pp : Format.formatter -> t -> unit
+230
lib/rtl433_output_mqtt.ml
···11+(* MQTT output handler using mqtte library *)
22+33+type topic_style =
44+ | Flat of string
55+ | Per_device of string
66+ | Per_field of string
77+ | Home_assistant of string
88+ | Custom of (Rtl433_data.t -> (string * string) list)
99+1010+type config = {
1111+ host : string;
1212+ port : int;
1313+ username : string option;
1414+ password : string option;
1515+ client_id : string option;
1616+ topic_style : topic_style;
1717+ retain : bool;
1818+ qos : [ `At_most_once | `At_least_once | `Exactly_once ];
1919+}
2020+2121+let default_config =
2222+ {
2323+ host = "localhost";
2424+ port = 1883;
2525+ username = None;
2626+ password = None;
2727+ client_id = None;
2828+ topic_style = Per_device "rtl433";
2929+ retain = false;
3030+ qos = `At_most_once;
3131+ }
3232+3333+let parse_url url =
3434+ (* Parse mqtt://[user:pass@]host[:port][,options] *)
3535+ try
3636+ let url =
3737+ if String.starts_with ~prefix:"mqtt://" url then
3838+ String.sub url 7 (String.length url - 7)
3939+ else url
4040+ in
4141+ (* Split by comma for options *)
4242+ let parts = String.split_on_char ',' url in
4343+ let host_part = List.hd parts in
4444+ let options = List.tl parts in
4545+4646+ (* Parse host part: [user:pass@]host[:port] *)
4747+ let user, pass, host_port =
4848+ match String.split_on_char '@' host_part with
4949+ | [ hp ] -> (None, None, hp)
5050+ | [ auth; hp ] -> (
5151+ match String.split_on_char ':' auth with
5252+ | [ u; p ] -> (Some u, Some p, hp)
5353+ | [ u ] -> (Some u, None, hp)
5454+ | _ -> (None, None, hp))
5555+ | _ -> (None, None, host_part)
5656+ in
5757+5858+ let host, port =
5959+ match String.split_on_char ':' host_port with
6060+ | [ h; p ] -> (h, int_of_string p)
6161+ | [ h ] -> (h, 1883)
6262+ | _ -> (host_port, 1883)
6363+ in
6464+6565+ (* Parse options *)
6666+ let config = ref { default_config with host; port; username = user; password = pass } in
6767+ List.iter
6868+ (fun opt ->
6969+ match String.split_on_char '=' opt with
7070+ | [ "retain"; "true" ] -> config := { !config with retain = true }
7171+ | [ "retain"; "false" ] -> config := { !config with retain = false }
7272+ | [ "qos"; "0" ] -> config := { !config with qos = `At_most_once }
7373+ | [ "qos"; "1" ] -> config := { !config with qos = `At_least_once }
7474+ | [ "qos"; "2" ] -> config := { !config with qos = `Exactly_once }
7575+ | [ "user"; u ] -> config := { !config with username = Some u }
7676+ | [ "pass"; p ] -> config := { !config with password = Some p }
7777+ | _ -> ())
7878+ options;
7979+8080+ Ok !config
8181+ with _ -> Error "Invalid MQTT URL format"
8282+8383+type t = {
8484+ config : config;
8585+ mutable client : Mqtte_eio.Client.t option;
8686+ mutable messages_sent : int;
8787+ mutable errors : int;
8888+}
8989+9090+let create ~sw ~net ~clock config =
9191+ (* Build credentials if username/password provided *)
9292+ let credentials =
9393+ match (config.username, config.password) with
9494+ | Some u, Some p -> Some (`Username_password (u, p))
9595+ | Some u, None -> Some (`Username u)
9696+ | None, _ -> None
9797+ in
9898+9999+ (* Generate client ID if not provided *)
100100+ let client_id =
101101+ match config.client_id with
102102+ | Some id -> id
103103+ | None -> Printf.sprintf "rtl433-%d" (Random.int 100000)
104104+ in
105105+106106+ let mqtt_config = Mqtte_eio.Client.{
107107+ (default_config ~client_id ()) with
108108+ credentials;
109109+ keep_alive = 60;
110110+ } in
111111+112112+ (* Try to connect *)
113113+ try
114114+ let client = Mqtte_eio.Client.connect
115115+ ~sw ~net ~clock
116116+ ~config:mqtt_config
117117+ ~host:config.host
118118+ ~port:config.port
119119+ ()
120120+ in
121121+ { config; client = Some client; messages_sent = 0; errors = 0 }
122122+ with exn ->
123123+ Logs.err (fun m -> m "MQTT connection failed: %a" Fmt.exn exn);
124124+ { config; client = None; messages_sent = 0; errors = 0 }
125125+126126+let get_topics config data =
127127+ match config.topic_style with
128128+ | Flat prefix -> [ (prefix, Rtl433_data.to_json data) ]
129129+ | Per_device prefix ->
130130+ let model =
131131+ match Rtl433_data.get_string "model" data with Some m -> m | None -> "unknown"
132132+ in
133133+ let id =
134134+ match Rtl433_data.get_string "id" data with
135135+ | Some i -> i
136136+ | None -> (
137137+ match Rtl433_data.get_int "id" data with
138138+ | Some i -> string_of_int i
139139+ | None -> "0")
140140+ in
141141+ [ (Printf.sprintf "%s/%s/%s" prefix model id, Rtl433_data.to_json data) ]
142142+ | Per_field prefix ->
143143+ let model =
144144+ match Rtl433_data.get_string "model" data with Some m -> m | None -> "unknown"
145145+ in
146146+ let id =
147147+ match Rtl433_data.get_string "id" data with
148148+ | Some i -> i
149149+ | None -> (
150150+ match Rtl433_data.get_int "id" data with
151151+ | Some i -> string_of_int i
152152+ | None -> "0")
153153+ in
154154+ List.map
155155+ (fun field ->
156156+ let topic = Printf.sprintf "%s/%s/%s/%s" prefix model id field.Rtl433_data.key in
157157+ let payload =
158158+ match field.Rtl433_data.value with
159159+ | Rtl433_data.Int i -> string_of_int i
160160+ | Rtl433_data.Float f -> string_of_float f
161161+ | Rtl433_data.String s -> s
162162+ | _ -> Rtl433_data.to_json [ field ]
163163+ in
164164+ (topic, payload))
165165+ data
166166+ | Home_assistant _prefix ->
167167+ (* TODO: Generate HA discovery config *)
168168+ [ ("", Rtl433_data.to_json data) ]
169169+ | Custom fn -> fn data
170170+171171+let output t data =
172172+ match t.client with
173173+ | None ->
174174+ t.errors <- t.errors + 1
175175+ | Some client ->
176176+ if Mqtte_eio.Client.is_connected client then begin
177177+ let topics = get_topics t.config data in
178178+ List.iter
179179+ (fun (topic, payload) ->
180180+ if topic <> "" then begin
181181+ try
182182+ Mqtte_eio.Client.publish
183183+ ~qos:t.config.qos
184184+ ~retain:t.config.retain
185185+ ~topic
186186+ payload
187187+ client;
188188+ t.messages_sent <- t.messages_sent + 1
189189+ with exn ->
190190+ Logs.err (fun m -> m "MQTT publish failed: %a" Fmt.exn exn);
191191+ t.errors <- t.errors + 1
192192+ end)
193193+ topics
194194+ end else
195195+ t.errors <- t.errors + 1
196196+197197+let close t =
198198+ match t.client with
199199+ | Some client ->
200200+ (try Mqtte_eio.Client.disconnect client with _ -> ());
201201+ t.client <- None
202202+ | None -> ()
203203+204204+let is_connected t =
205205+ match t.client with
206206+ | Some client -> Mqtte_eio.Client.is_connected client
207207+ | None -> false
208208+209209+let get_stats t =
210210+ Rtl433_data.make
211211+ [
212212+ ("messages_sent", None, Rtl433_data.Int t.messages_sent);
213213+ ("errors", None, Rtl433_data.Int t.errors);
214214+ ("connected", None, Rtl433_data.String (if is_connected t then "true" else "false"));
215215+ ]
216216+217217+let to_output t =
218218+ Rtl433_output.custom (fun data -> output t data)
219219+220220+let pp_config fmt config =
221221+ Format.fprintf fmt "MQTT<%s:%d retain=%b qos=%d>" config.host config.port
222222+ config.retain
223223+ (match config.qos with
224224+ | `At_most_once -> 0
225225+ | `At_least_once -> 1
226226+ | `Exactly_once -> 2)
227227+228228+let pp fmt t =
229229+ Format.fprintf fmt "%a connected=%b msgs=%d" pp_config t.config (is_connected t)
230230+ t.messages_sent
+123
lib/rtl433_output_mqtt.mli
···11+(** MQTT output handler for publishing decoded sensor data.
22+33+ This module integrates with the ocaml-mqtte library to publish
44+ decoded sensor data to an MQTT broker. *)
55+66+(** {1 Configuration} *)
77+88+(** MQTT topic style for publishing. *)
99+type topic_style =
1010+ | Flat of string
1111+ (** Single topic, all data as JSON.
1212+ E.g., "rtl433" -> all messages to "rtl433" *)
1313+ | Per_device of string
1414+ (** Topic per device model and ID.
1515+ E.g., "rtl433" -> "rtl433/Fineoffset-WH51/abc123" *)
1616+ | Per_field of string
1717+ (** Topic per device and field.
1818+ E.g., "rtl433" -> "rtl433/Fineoffset-WH51/abc123/temperature_C" *)
1919+ | Home_assistant of string
2020+ (** Home Assistant auto-discovery compatible.
2121+ Creates config topics and state topics. *)
2222+ | Custom of (Rtl433_data.t -> (string * string) list)
2323+ (** Custom function returning (topic, payload) pairs. *)
2424+2525+(** MQTT connection configuration. *)
2626+type config = {
2727+ host : string;
2828+ (** Broker hostname. *)
2929+ port : int;
3030+ (** Broker port (default 1883). *)
3131+ username : string option;
3232+ (** Username for authentication. *)
3333+ password : string option;
3434+ (** Password for authentication. *)
3535+ client_id : string option;
3636+ (** Client ID (auto-generated if None). *)
3737+ topic_style : topic_style;
3838+ (** How to structure topics. *)
3939+ retain : bool;
4040+ (** Retain flag for published messages. *)
4141+ qos : [ `At_most_once | `At_least_once | `Exactly_once ];
4242+ (** Quality of service level. *)
4343+}
4444+4545+(** Default configuration. *)
4646+val default_config : config
4747+4848+(** {1 Parsing Configuration} *)
4949+5050+(** Parse MQTT URL configuration string.
5151+5252+ Format: mqtt://[user:pass@]host[:port][/topic][,options]
5353+5454+ Options:
5555+ - retain=true/false
5656+ - qos=0/1/2
5757+ - topic_style=flat/device/field/ha
5858+5959+ Example: "mqtt://sensor:pass@192.168.1.1:1883,retain=true"
6060+6161+ @param url URL string
6262+ @return Parsed config or error message *)
6363+val parse_url : string -> (config, string) result
6464+6565+(** {1 Output Handler} *)
6666+6767+(** MQTT output handler. *)
6868+type t
6969+7070+(** Create an MQTT output handler.
7171+7272+ @param sw Eio switch for resource management
7373+ @param net Network capability
7474+ @param clock Clock capability
7575+ @param config MQTT configuration
7676+ @return Output handler *)
7777+val create :
7878+ sw:Eio.Switch.t ->
7979+ net:_ Eio.Net.t ->
8080+ clock:_ Eio.Time.clock ->
8181+ config ->
8282+ t
8383+8484+(** Output decoded data to MQTT.
8585+8686+ @param handler MQTT handler
8787+ @param data Decoded sensor data *)
8888+val output : t -> Rtl433_data.t -> unit
8989+9090+(** Close the MQTT connection.
9191+9292+ @param handler MQTT handler *)
9393+val close : t -> unit
9494+9595+(** Check if connected to broker.
9696+9797+ @param handler MQTT handler
9898+ @return True if connected *)
9999+val is_connected : t -> bool
100100+101101+(** {1 Statistics} *)
102102+103103+(** Get publishing statistics.
104104+105105+ @param handler MQTT handler
106106+ @return Data with message counts, errors, etc. *)
107107+val get_stats : t -> Rtl433_data.t
108108+109109+(** {1 As Generic Output} *)
110110+111111+(** Convert to generic output handler.
112112+113113+ @param handler MQTT handler
114114+ @return Generic output handler *)
115115+val to_output : t -> Rtl433_output.t
116116+117117+(** {1 Printing} *)
118118+119119+(** Pretty-print configuration. *)
120120+val pp_config : Format.formatter -> config -> unit
121121+122122+(** Pretty-print handler info. *)
123123+val pp : Format.formatter -> t -> unit
+231
lib/rtl433_pipeline.ml
···11+(* Signal processing pipeline *)
22+33+type stats = {
44+ frames_processed : int;
55+ frames_with_signal : int;
66+ pulses_detected : int;
77+ decode_attempts : int;
88+ decode_successes : int;
99+ messages_output : int;
1010+ bytes_processed : int64;
1111+ running_time_s : float;
1212+}
1313+1414+type event =
1515+ | Signal_detected of Rtl433_pulse_data.t
1616+ | Message_decoded of Rtl433_data.t
1717+ | Decode_failed of string * Rtl433_device.decode_result
1818+ | Frequency_changed of int
1919+ | Input_error of exn
2020+ | Stopped
2121+2222+type t = {
2323+ config : Rtl433_config.t;
2424+ input : Rtl433_input.t;
2525+ outputs : Rtl433_output.t list;
2626+ registry : Rtl433_registry.t;
2727+ mutable running : bool;
2828+ mutable stats : stats;
2929+ mutable event_callback : (event -> unit) option;
3030+ mutable current_frequency : int;
3131+ pulse_detector : Rtl433_pulse_detect.t;
3232+}
3333+3434+let empty_stats =
3535+ {
3636+ frames_processed = 0;
3737+ frames_with_signal = 0;
3838+ pulses_detected = 0;
3939+ decode_attempts = 0;
4040+ decode_successes = 0;
4141+ messages_output = 0;
4242+ bytes_processed = 0L;
4343+ running_time_s = 0.0;
4444+ }
4545+4646+let create ~(config : Rtl433_config.t) ~input ~outputs ~registry =
4747+ let sample_rate = Rtl433_input.sample_rate input in
4848+ let mode =
4949+ (* Determine detection mode from enabled decoders *)
5050+ let decoders = Rtl433_registry.enabled registry in
5151+ if
5252+ List.exists
5353+ (fun d -> Rtl433_modulation.is_fsk d.Rtl433_device.modulation)
5454+ decoders
5555+ then Rtl433_pulse_detect.FSK
5656+ else Rtl433_pulse_detect.OOK
5757+ in
5858+ let pulse_detector = Rtl433_pulse_detect.create ~mode ~sample_rate () in
5959+ let current_frequency =
6060+ match config.frequencies with f :: _ -> f | [] -> 433920000
6161+ in
6262+ {
6363+ config;
6464+ input;
6565+ outputs;
6666+ registry;
6767+ running = false;
6868+ stats = empty_stats;
6969+ event_callback = None;
7070+ current_frequency;
7171+ pulse_detector;
7272+ }
7373+7474+let emit_event t event =
7575+ match t.event_callback with Some cb -> cb event | None -> ()
7676+7777+let process_pulse_data t pulse_data =
7878+ t.stats <- { t.stats with pulses_detected = t.stats.pulses_detected + 1 };
7979+ emit_event t (Signal_detected pulse_data);
8080+8181+ (* Run decoders *)
8282+ let results = Rtl433_registry.run_decoders t.registry pulse_data () in
8383+ List.iter
8484+ (fun (decoder, result) ->
8585+ t.stats <- { t.stats with decode_attempts = t.stats.decode_attempts + 1 };
8686+ match result with
8787+ | Rtl433_device.Decoded n ->
8888+ t.stats <-
8989+ {
9090+ t.stats with
9191+ decode_successes = t.stats.decode_successes + 1;
9292+ messages_output = t.stats.messages_output + n;
9393+ };
9494+ (* Output decoded data to all outputs *)
9595+ (match Rtl433_data.get_last_output () with
9696+ | Some data ->
9797+ List.iter (fun output -> Rtl433_output.output output data) t.outputs;
9898+ emit_event t (Message_decoded data);
9999+ Rtl433_data.clear_last_output ()
100100+ | None -> ())
101101+ | _ -> emit_event t (Decode_failed (decoder.Rtl433_device.name, result)))
102102+ results
103103+104104+let process_buffer t samples =
105105+ let len = Bytes.length samples in
106106+ t.stats <-
107107+ {
108108+ t.stats with
109109+ frames_processed = t.stats.frames_processed + 1;
110110+ bytes_processed = Int64.add t.stats.bytes_processed (Int64.of_int len);
111111+ };
112112+113113+ (* Demodulate *)
114114+ let format = Rtl433_input.format t.input in
115115+ let envelope = Rtl433_baseband.envelope_detect format samples in
116116+117117+ (* Detect pulses *)
118118+ let pulse_datas = Rtl433_pulse_detect.process t.pulse_detector envelope in
119119+ if pulse_datas <> [] then
120120+ t.stats <-
121121+ { t.stats with frames_with_signal = t.stats.frames_with_signal + 1 };
122122+123123+ (* Process each detected packet *)
124124+ List.iter (process_pulse_data t) pulse_datas;
125125+126126+ List.length pulse_datas
127127+128128+let run t ~clock =
129129+ t.running <- true;
130130+ let start_time = Eio.Time.now clock in
131131+ let buf = Bytes.make (16 * 32 * 512) '\000' in
132132+133133+ while t.running do
134134+ try
135135+ let n = Rtl433_input.read t.input buf in
136136+ if n = 0 then t.running <- false
137137+ else begin
138138+ let samples = Bytes.sub buf 0 n in
139139+ ignore (process_buffer t samples)
140140+ end
141141+ with
142142+ | End_of_file ->
143143+ (* Normal end of file, not an error *)
144144+ t.running <- false
145145+ | exn ->
146146+ emit_event t (Input_error exn);
147147+ t.running <- false
148148+ done;
149149+150150+ (* Flush any remaining pulses *)
151151+ (match Rtl433_pulse_detect.flush t.pulse_detector with
152152+ | Some pulse_data -> process_pulse_data t pulse_data
153153+ | None -> ());
154154+155155+ let end_time = Eio.Time.now clock in
156156+ t.stats <- { t.stats with running_time_s = end_time -. start_time };
157157+ emit_event t Stopped
158158+159159+let run_for t ~clock ~duration_s =
160160+ t.running <- true;
161161+ let start_time = Eio.Time.now clock in
162162+ let buf = Bytes.make (16 * 32 * 512) '\000' in
163163+164164+ while t.running && Eio.Time.now clock -. start_time < duration_s do
165165+ try
166166+ let n =
167167+ Rtl433_input.read_timeout t.input buf ~clock ~timeout_ms:100.0
168168+ in
169169+ if n > 0 then begin
170170+ let samples = Bytes.sub buf 0 n in
171171+ ignore (process_buffer t samples)
172172+ end
173173+ with exn ->
174174+ emit_event t (Input_error exn);
175175+ t.running <- false
176176+ done;
177177+178178+ t.running <- false;
179179+ let end_time = Eio.Time.now clock in
180180+ t.stats <- { t.stats with running_time_s = end_time -. start_time };
181181+ emit_event t Stopped
182182+183183+let stop t = t.running <- false
184184+let is_running t = t.running
185185+186186+let set_frequency t freq =
187187+ Rtl433_input.set_frequency t.input freq;
188188+ t.current_frequency <- freq;
189189+ emit_event t (Frequency_changed freq)
190190+191191+let get_frequency t = t.current_frequency
192192+193193+let enable_hopping _t ~frequencies ~interval_s =
194194+ ignore (frequencies, interval_s);
195195+ (* TODO: Implement frequency hopping *)
196196+ ()
197197+198198+let get_stats t = t.stats
199199+let reset_stats t = t.stats <- empty_stats
200200+201201+let stats_to_data t =
202202+ Rtl433_data.make
203203+ [
204204+ ( "frames_processed",
205205+ Some "Frames",
206206+ Rtl433_data.Int t.stats.frames_processed );
207207+ ("frames_with_signal", Some "Signal", Rtl433_data.Int t.stats.frames_with_signal);
208208+ ("pulses_detected", Some "Pulses", Rtl433_data.Int t.stats.pulses_detected);
209209+ ("decode_successes", Some "Decoded", Rtl433_data.Int t.stats.decode_successes);
210210+ ("messages_output", Some "Messages", Rtl433_data.Int t.stats.messages_output);
211211+ ( "bytes_processed",
212212+ Some "Bytes",
213213+ Rtl433_data.Int (Int64.to_int t.stats.bytes_processed) );
214214+ ( "running_time_s",
215215+ Some "Time",
216216+ Rtl433_data.Float t.stats.running_time_s );
217217+ ]
218218+219219+let on_event t callback = t.event_callback <- Some callback
220220+221221+let rec pp fmt t =
222222+ Format.fprintf fmt "Pipeline<%s> running=%b@." (if t.running then "active" else "idle")
223223+ t.running;
224224+ pp_stats fmt t.stats
225225+226226+and pp_stats fmt stats =
227227+ Format.fprintf fmt
228228+ "Stats: frames=%d signal=%d pulses=%d decoded=%d msgs=%d bytes=%Ld time=%.1fs@."
229229+ stats.frames_processed stats.frames_with_signal stats.pulses_detected
230230+ stats.decode_successes stats.messages_output stats.bytes_processed
231231+ stats.running_time_s
+165
lib/rtl433_pipeline.mli
···11+(** Signal processing pipeline.
22+33+ This module orchestrates the complete signal processing chain:
44+ Input -> Baseband -> Pulse Detection -> Decoding -> Output *)
55+66+(** {1 Types} *)
77+88+(** Pipeline handle. *)
99+type t
1010+1111+(** Pipeline statistics. *)
1212+type stats = {
1313+ frames_processed : int;
1414+ (** Total frames processed. *)
1515+ frames_with_signal : int;
1616+ (** Frames with detected signal. *)
1717+ pulses_detected : int;
1818+ (** Total pulse packages detected. *)
1919+ decode_attempts : int;
2020+ (** Total decode attempts. *)
2121+ decode_successes : int;
2222+ (** Successful decodes. *)
2323+ messages_output : int;
2424+ (** Messages sent to output. *)
2525+ bytes_processed : int64;
2626+ (** Total bytes processed. *)
2727+ running_time_s : float;
2828+ (** Total running time in seconds. *)
2929+}
3030+3131+(** {1 Construction} *)
3232+3333+(** Create a new pipeline.
3434+3535+ @param config Configuration
3636+ @param input Input source
3737+ @param outputs Output handlers
3838+ @param registry Decoder registry
3939+ @return Pipeline handle *)
4040+val create :
4141+ config:Rtl433_config.t ->
4242+ input:Rtl433_input.t ->
4343+ outputs:Rtl433_output.t list ->
4444+ registry:Rtl433_registry.t ->
4545+ t
4646+4747+(** {1 Running} *)
4848+4949+(** Run the pipeline.
5050+5151+ Processes samples from the input, detects pulses, runs decoders,
5252+ and sends decoded data to outputs. Runs until input EOF or
5353+ stop is called.
5454+5555+ @param pipeline Pipeline handle
5656+ @param clock Clock capability for timing *)
5757+val run : t -> clock:_ Eio.Time.clock -> unit
5858+5959+(** Run the pipeline for a specific duration.
6060+6161+ @param pipeline Pipeline handle
6262+ @param clock Clock capability
6363+ @param duration_s Maximum duration in seconds *)
6464+val run_for : t -> clock:_ Eio.Time.clock -> duration_s:float -> unit
6565+6666+(** Process a single buffer of samples.
6767+6868+ Useful for testing or custom integration.
6969+7070+ @param pipeline Pipeline handle
7171+ @param samples Sample buffer
7272+ @return Number of messages decoded *)
7373+val process_buffer : t -> bytes -> int
7474+7575+(** {1 Control} *)
7676+7777+(** Stop the pipeline.
7878+7979+ Signals the pipeline to stop processing. The run function
8080+ will return after completing the current buffer. *)
8181+val stop : t -> unit
8282+8383+(** Check if pipeline is running.
8484+8585+ @param pipeline Pipeline handle
8686+ @return True if currently running *)
8787+val is_running : t -> bool
8888+8989+(** {1 Frequency Control} *)
9090+9191+(** Set the center frequency.
9292+9393+ For rtl_tcp inputs, sends command to change frequency.
9494+ For file inputs, this is a no-op.
9595+9696+ @param pipeline Pipeline handle
9797+ @param freq Frequency in Hz *)
9898+val set_frequency : t -> int -> unit
9999+100100+(** Get the current center frequency.
101101+102102+ @param pipeline Pipeline handle
103103+ @return Current frequency in Hz *)
104104+val get_frequency : t -> int
105105+106106+(** Enable frequency hopping.
107107+108108+ @param pipeline Pipeline handle
109109+ @param frequencies List of frequencies in Hz
110110+ @param interval_s Hop interval in seconds *)
111111+val enable_hopping :
112112+ t ->
113113+ frequencies:int list ->
114114+ interval_s:float ->
115115+ unit
116116+117117+(** {1 Statistics} *)
118118+119119+(** Get current statistics.
120120+121121+ @param pipeline Pipeline handle
122122+ @return Current statistics *)
123123+val get_stats : t -> stats
124124+125125+(** Reset statistics.
126126+127127+ @param pipeline Pipeline handle *)
128128+val reset_stats : t -> unit
129129+130130+(** Get statistics as data structure.
131131+132132+ @param pipeline Pipeline handle
133133+ @return Statistics as Rtl433_data.t *)
134134+val stats_to_data : t -> Rtl433_data.t
135135+136136+(** {1 Events} *)
137137+138138+(** Event callback type. *)
139139+type event =
140140+ | Signal_detected of Rtl433_pulse_data.t
141141+ (** Pulse package detected. *)
142142+ | Message_decoded of Rtl433_data.t
143143+ (** Message successfully decoded. *)
144144+ | Decode_failed of string * Rtl433_device.decode_result
145145+ (** Decode failed with decoder name and result. *)
146146+ | Frequency_changed of int
147147+ (** Frequency changed (for hopping). *)
148148+ | Input_error of exn
149149+ (** Error reading from input. *)
150150+ | Stopped
151151+ (** Pipeline stopped. *)
152152+153153+(** Set event callback.
154154+155155+ @param pipeline Pipeline handle
156156+ @param callback Function called for each event *)
157157+val on_event : t -> (event -> unit) -> unit
158158+159159+(** {1 Printing} *)
160160+161161+(** Pretty-print pipeline status. *)
162162+val pp : Format.formatter -> t -> unit
163163+164164+(** Pretty-print statistics. *)
165165+val pp_stats : Format.formatter -> stats -> unit
+94
lib/rtl433_pulse_data.ml
···11+(* Pulse timing data *)
22+33+let max_pulses = 1200
44+let min_pulses = 16
55+66+type t = {
77+ offset : int64;
88+ sample_rate : int;
99+ depth_bits : int;
1010+ start_ago : int;
1111+ end_ago : int;
1212+ num_pulses : int;
1313+ pulses : int array;
1414+ gaps : int array;
1515+ ook_low_estimate : int;
1616+ ook_high_estimate : int;
1717+ fsk_f1_est : int;
1818+ fsk_f2_est : int;
1919+ freq1_hz : float;
2020+ freq2_hz : float;
2121+ centerfreq_hz : float;
2222+ range_db : float;
2323+ rssi_db : float;
2424+ snr_db : float;
2525+ noise_db : float;
2626+}
2727+2828+let create ?(sample_rate = 250000) () =
2929+ {
3030+ offset = 0L;
3131+ sample_rate;
3232+ depth_bits = 8;
3333+ start_ago = 0;
3434+ end_ago = 0;
3535+ num_pulses = 0;
3636+ pulses = Array.make max_pulses 0;
3737+ gaps = Array.make max_pulses 0;
3838+ ook_low_estimate = 0;
3939+ ook_high_estimate = 0;
4040+ fsk_f1_est = 0;
4141+ fsk_f2_est = 0;
4242+ freq1_hz = 0.0;
4343+ freq2_hz = 0.0;
4444+ centerfreq_hz = 0.0;
4545+ range_db = 0.0;
4646+ rssi_db = 0.0;
4747+ snr_db = 0.0;
4848+ noise_db = 0.0;
4949+ }
5050+5151+let clear t = { t with num_pulses = 0 }
5252+5353+let add_pulse t ~pulse ~gap =
5454+ if t.num_pulses >= max_pulses then None
5555+ else begin
5656+ t.pulses.(t.num_pulses) <- pulse;
5757+ t.gaps.(t.num_pulses) <- gap;
5858+ Some { t with num_pulses = t.num_pulses + 1 }
5959+ end
6060+6161+let samples_to_us t samples =
6262+ Float.of_int samples *. 1_000_000.0 /. Float.of_int t.sample_rate
6363+6464+let us_to_samples t us = int_of_float (us *. Float.of_int t.sample_rate /. 1_000_000.0)
6565+6666+let pulse_us t idx =
6767+ if idx < t.num_pulses then samples_to_us t t.pulses.(idx) else 0.0
6868+6969+let gap_us t idx =
7070+ if idx < t.num_pulses then samples_to_us t t.gaps.(idx) else 0.0
7171+7272+let total_samples t =
7373+ let sum = ref 0 in
7474+ for i = 0 to t.num_pulses - 1 do
7575+ sum := !sum + t.pulses.(i) + t.gaps.(i)
7676+ done;
7777+ !sum
7878+7979+let total_us t = samples_to_us t (total_samples t)
8080+8181+let exceeds_gap_limit t gap_limit_us =
8282+ let limit_samples = us_to_samples t gap_limit_us in
8383+ let rec check i =
8484+ if i >= t.num_pulses then false
8585+ else if t.gaps.(i) > limit_samples then true
8686+ else check (i + 1)
8787+ in
8888+ check 0
8989+9090+let pp fmt t =
9191+ Format.fprintf fmt "PulseData: %d pulses, %.1f us total@." t.num_pulses
9292+ (total_us t)
9393+9494+let to_string t = Format.asprintf "%a" pp t
+131
lib/rtl433_pulse_data.mli
···11+(** Pulse timing data from demodulated RF signals.
22+33+ This module represents the raw pulse and gap timings extracted
44+ from demodulated RF signals. The pulses and gaps are measured
55+ in sample counts at the configured sample rate. *)
66+77+(** {1 Constants} *)
88+99+(** Maximum number of pulses before forcing end of package. *)
1010+val max_pulses : int
1111+1212+(** Minimum number of pulses for a valid package. *)
1313+val min_pulses : int
1414+1515+(** {1 Types} *)
1616+1717+(** Pulse data containing timing information from a received signal. *)
1818+type t = {
1919+ offset : int64;
2020+ (** Offset to first pulse in samples from start of stream. *)
2121+ sample_rate : int;
2222+ (** Sample rate the pulses were recorded at (Hz). *)
2323+ depth_bits : int;
2424+ (** Sample depth in bits (typically 8). *)
2525+ start_ago : int;
2626+ (** Start of first pulse in samples ago. *)
2727+ end_ago : int;
2828+ (** End of last pulse in samples ago. *)
2929+ num_pulses : int;
3030+ (** Number of pulses recorded. *)
3131+ pulses : int array;
3232+ (** Width of pulses (high periods) in samples. *)
3333+ gaps : int array;
3434+ (** Width of gaps (low periods) between pulses in samples. *)
3535+ ook_low_estimate : int;
3636+ (** Estimated OOK low level (noise floor). *)
3737+ ook_high_estimate : int;
3838+ (** Estimated OOK high level (signal). *)
3939+ fsk_f1_est : int;
4040+ (** Estimated F1 frequency for FSK (Hz offset). *)
4141+ fsk_f2_est : int;
4242+ (** Estimated F2 frequency for FSK (Hz offset). *)
4343+ freq1_hz : float;
4444+ (** Absolute F1 frequency (Hz). *)
4545+ freq2_hz : float;
4646+ (** Absolute F2 frequency (Hz). *)
4747+ centerfreq_hz : float;
4848+ (** Center frequency (Hz). *)
4949+ range_db : float;
5050+ (** Signal range in dB. *)
5151+ rssi_db : float;
5252+ (** Received signal strength indicator (dBm). *)
5353+ snr_db : float;
5454+ (** Signal to noise ratio (dB). *)
5555+ noise_db : float;
5656+ (** Noise floor (dBm). *)
5757+}
5858+5959+(** {1 Construction} *)
6060+6161+(** Create empty pulse data for a given sample rate.
6262+6363+ @param sample_rate Sample rate in Hz (default 250000)
6464+ @return Empty pulse data structure *)
6565+val create : ?sample_rate:int -> unit -> t
6666+6767+(** Clear all pulses from the structure, keeping configuration. *)
6868+val clear : t -> t
6969+7070+(** {1 Adding Pulses} *)
7171+7272+(** Add a pulse and gap to the data.
7373+7474+ @param pulse_data The pulse data to modify
7575+ @param pulse Pulse width in samples
7676+ @param gap Gap width in samples
7777+ @return Updated pulse data, or None if max pulses reached *)
7878+val add_pulse : t -> pulse:int -> gap:int -> t option
7979+8080+(** {1 Timing Conversions} *)
8181+8282+(** Convert samples to microseconds.
8383+8484+ @param pulse_data Pulse data with sample rate
8585+ @param samples Number of samples
8686+ @return Time in microseconds *)
8787+val samples_to_us : t -> int -> float
8888+8989+(** Convert microseconds to samples.
9090+9191+ @param pulse_data Pulse data with sample rate
9292+ @param us Time in microseconds
9393+ @return Number of samples *)
9494+val us_to_samples : t -> float -> int
9595+9696+(** Get pulse width in microseconds.
9797+9898+ @param pulse_data Pulse data
9999+ @param idx Pulse index
100100+ @return Pulse width in microseconds *)
101101+val pulse_us : t -> int -> float
102102+103103+(** Get gap width in microseconds.
104104+105105+ @param pulse_data Pulse data
106106+ @param idx Gap index (same as pulse index)
107107+ @return Gap width in microseconds *)
108108+val gap_us : t -> int -> float
109109+110110+(** {1 Analysis} *)
111111+112112+(** Calculate total duration in samples. *)
113113+val total_samples : t -> int
114114+115115+(** Calculate total duration in microseconds. *)
116116+val total_us : t -> float
117117+118118+(** Check if pulse data exceeds a gap limit.
119119+120120+ @param pulse_data Pulse data
121121+ @param gap_limit_us Maximum gap in microseconds
122122+ @return True if any gap exceeds the limit *)
123123+val exceeds_gap_limit : t -> float -> bool
124124+125125+(** {1 Printing} *)
126126+127127+(** Pretty-print pulse data. *)
128128+val pp : Format.formatter -> t -> unit
129129+130130+(** Convert to a human-readable string summary. *)
131131+val to_string : t -> string
+187
lib/rtl433_pulse_detect.ml
···11+(* Pulse detection - converts envelope signal to pulse/gap timing *)
22+33+type mode = OOK | FSK
44+55+type state =
66+ | Idle
77+ | In_pulse of int (* samples in current pulse *)
88+ | In_gap of int (* samples in current gap *)
99+1010+type t = {
1111+ mode : mode;
1212+ sample_rate : int;
1313+ mutable level_limit : int;
1414+ mutable min_pulse_samples : int;
1515+ mutable max_gap_samples : int;
1616+ mutable reset_gap_samples : int;
1717+ mutable level_estimate : int;
1818+ mutable noise_estimate : int;
1919+ mutable center_frequency : int;
2020+ mutable state : state;
2121+ mutable current_pulse_data : Rtl433_pulse_data.t;
2222+ mutable sample_offset : int64;
2323+}
2424+2525+let create ~mode ~sample_rate ?(level_limit = 0) () =
2626+ let default_level = if level_limit = 0 then
2727+ match mode with
2828+ | OOK -> 140 (* threshold for OOK above noise *)
2929+ | FSK -> 128 (* midpoint for FSK *)
3030+ else level_limit
3131+ in
3232+ {
3333+ mode;
3434+ sample_rate;
3535+ level_limit = default_level;
3636+ min_pulse_samples = sample_rate / 50000; (* ~20us min pulse *)
3737+ max_gap_samples = sample_rate / 200; (* 5ms max gap within packet *)
3838+ reset_gap_samples = sample_rate / 10; (* 100ms reset between packets *)
3939+ level_estimate = 128;
4040+ noise_estimate = 10;
4141+ center_frequency = 0;
4242+ state = Idle;
4343+ current_pulse_data = Rtl433_pulse_data.create ~sample_rate ();
4444+ sample_offset = 0L;
4545+ }
4646+4747+let reset t =
4848+ t.state <- Idle;
4949+ t.current_pulse_data <- Rtl433_pulse_data.create ~sample_rate:t.sample_rate ()
5050+5151+let finish_packet t =
5252+ if t.current_pulse_data.num_pulses >= Rtl433_pulse_data.min_pulses then
5353+ let pd = t.current_pulse_data in
5454+ t.current_pulse_data <- Rtl433_pulse_data.create ~sample_rate:t.sample_rate ();
5555+ Some pd
5656+ else begin
5757+ t.current_pulse_data <- Rtl433_pulse_data.create ~sample_rate:t.sample_rate ();
5858+ None
5959+ end
6060+6161+let _add_pulse_gap t ~pulse ~gap =
6262+ if pulse >= t.min_pulse_samples then begin
6363+ match Rtl433_pulse_data.add_pulse t.current_pulse_data ~pulse ~gap with
6464+ | Some pd -> t.current_pulse_data <- pd; None
6565+ | None ->
6666+ (* Buffer full, emit packet and start new one *)
6767+ let result = finish_packet t in
6868+ (match Rtl433_pulse_data.add_pulse t.current_pulse_data ~pulse ~gap with
6969+ | Some pd -> t.current_pulse_data <- pd
7070+ | None -> ());
7171+ result
7272+ end else None
7373+7474+let process t samples =
7575+ let len = Bytes.length samples in
7676+ let results = ref [] in
7777+7878+ (* Update level estimate using low-pass filter *)
7979+ if len > 0 then begin
8080+ let sum = ref 0 in
8181+ for i = 0 to min 1000 len - 1 do
8282+ sum := !sum + Bytes.get_uint8 samples i
8383+ done;
8484+ let avg = !sum / min 1000 len in
8585+ t.level_estimate <- (t.level_estimate * 7 + avg) / 8
8686+ end;
8787+8888+ for i = 0 to len - 1 do
8989+ let level = Bytes.get_uint8 samples i in
9090+ let is_high = level > t.level_limit in
9191+9292+ match t.state with
9393+ | Idle ->
9494+ if is_high then
9595+ t.state <- In_pulse 1
9696+9797+ | In_pulse pulse_samples ->
9898+ if is_high then
9999+ t.state <- In_pulse (pulse_samples + 1)
100100+ else begin
101101+ (* Pulse ended, start counting gap *)
102102+ t.state <- In_gap 1;
103103+ (* Record pulse width, gap will be filled in later *)
104104+ if pulse_samples >= t.min_pulse_samples then begin
105105+ t.current_pulse_data.pulses.(t.current_pulse_data.num_pulses) <- pulse_samples
106106+ end
107107+ end
108108+109109+ | In_gap gap_samples ->
110110+ if is_high then begin
111111+ (* New pulse started - record the gap from previous pulse *)
112112+ if t.current_pulse_data.num_pulses < Rtl433_pulse_data.max_pulses &&
113113+ t.current_pulse_data.pulses.(t.current_pulse_data.num_pulses) > 0 then begin
114114+ t.current_pulse_data.gaps.(t.current_pulse_data.num_pulses) <- gap_samples;
115115+ t.current_pulse_data <- { t.current_pulse_data with
116116+ num_pulses = t.current_pulse_data.num_pulses + 1
117117+ }
118118+ end;
119119+ t.state <- In_pulse 1
120120+ end
121121+ else if gap_samples > t.reset_gap_samples then begin
122122+ (* Long gap - end of packet *)
123123+ (* Record final gap *)
124124+ if t.current_pulse_data.num_pulses < Rtl433_pulse_data.max_pulses &&
125125+ t.current_pulse_data.pulses.(t.current_pulse_data.num_pulses) > 0 then begin
126126+ t.current_pulse_data.gaps.(t.current_pulse_data.num_pulses) <- gap_samples;
127127+ t.current_pulse_data <- { t.current_pulse_data with
128128+ num_pulses = t.current_pulse_data.num_pulses + 1
129129+ }
130130+ end;
131131+ (match finish_packet t with
132132+ | Some pd -> results := pd :: !results
133133+ | None -> ());
134134+ t.state <- Idle
135135+ end
136136+ else
137137+ t.state <- In_gap (gap_samples + 1)
138138+ done;
139139+140140+ t.sample_offset <- Int64.add t.sample_offset (Int64.of_int len);
141141+ List.rev !results
142142+143143+let flush t =
144144+ (* Handle any pending pulse *)
145145+ (match t.state with
146146+ | In_pulse pulse_samples ->
147147+ if pulse_samples >= t.min_pulse_samples &&
148148+ t.current_pulse_data.num_pulses < Rtl433_pulse_data.max_pulses then begin
149149+ t.current_pulse_data.pulses.(t.current_pulse_data.num_pulses) <- pulse_samples;
150150+ t.current_pulse_data.gaps.(t.current_pulse_data.num_pulses) <- 0;
151151+ t.current_pulse_data <- { t.current_pulse_data with
152152+ num_pulses = t.current_pulse_data.num_pulses + 1
153153+ }
154154+ end
155155+ | In_gap gap_samples ->
156156+ if t.current_pulse_data.num_pulses < Rtl433_pulse_data.max_pulses &&
157157+ t.current_pulse_data.pulses.(t.current_pulse_data.num_pulses) > 0 then begin
158158+ t.current_pulse_data.gaps.(t.current_pulse_data.num_pulses) <- gap_samples;
159159+ t.current_pulse_data <- { t.current_pulse_data with
160160+ num_pulses = t.current_pulse_data.num_pulses + 1
161161+ }
162162+ end
163163+ | Idle -> ());
164164+ t.state <- Idle;
165165+ finish_packet t
166166+167167+let set_level_limit t level = t.level_limit <- level
168168+let set_min_pulse_samples t samples = t.min_pulse_samples <- samples
169169+170170+let set_max_gap_ms t ms =
171171+ t.max_gap_samples <- int_of_float (ms *. Float.of_int t.sample_rate /. 1000.0)
172172+173173+let _set_reset_gap_ms t ms =
174174+ t.reset_gap_samples <- int_of_float (ms *. Float.of_int t.sample_rate /. 1000.0)
175175+176176+let get_level_estimate t = t.level_estimate
177177+let get_noise_estimate t = t.noise_estimate
178178+let get_center_frequency t = t.center_frequency
179179+180180+let pp fmt t =
181181+ Format.fprintf fmt "PulseDetect<%s> rate=%d level=%d state=%s"
182182+ (match t.mode with OOK -> "OOK" | FSK -> "FSK")
183183+ t.sample_rate t.level_limit
184184+ (match t.state with
185185+ | Idle -> "idle"
186186+ | In_pulse n -> Printf.sprintf "pulse(%d)" n
187187+ | In_gap n -> Printf.sprintf "gap(%d)" n)
+95
lib/rtl433_pulse_detect.mli
···11+(** Pulse detection from baseband signals.
22+33+ This module detects pulses and gaps in demodulated baseband signals,
44+ handling both OOK (amplitude-based) and FSK (frequency-based) signals. *)
55+66+(** {1 Types} *)
77+88+(** Detection mode. *)
99+type mode =
1010+ | OOK
1111+ (** On-Off Keying detection based on amplitude threshold. *)
1212+ | FSK
1313+ (** Frequency Shift Keying detection based on frequency deviation. *)
1414+1515+(** Pulse detector state. *)
1616+type t
1717+1818+(** {1 Construction} *)
1919+2020+(** Create a new pulse detector.
2121+2222+ @param mode Detection mode (OOK or FSK)
2323+ @param sample_rate Sample rate in Hz
2424+ @param level_limit Minimum signal level (0-255 for 8-bit)
2525+ @return New detector instance *)
2626+val create : mode:mode -> sample_rate:int -> ?level_limit:int -> unit -> t
2727+2828+(** Reset the detector state.
2929+3030+ Clears any partial pulse data and resets level estimates. *)
3131+val reset : t -> unit
3232+3333+(** {1 Detection} *)
3434+3535+(** Process samples and detect pulses.
3636+3737+ @param detector The detector
3838+ @param samples Baseband samples (envelope for OOK, FM for FSK)
3939+ @return List of complete pulse data packages detected *)
4040+val process : t -> bytes -> Rtl433_pulse_data.t list
4141+4242+(** Force end of current package.
4343+4444+ Call this when the sample stream ends to emit any partial data.
4545+4646+ @param detector The detector
4747+ @return Pulse data if any pulses were accumulated, None otherwise *)
4848+val flush : t -> Rtl433_pulse_data.t option
4949+5050+(** {1 Configuration} *)
5151+5252+(** Set the signal level threshold for OOK detection.
5353+5454+ @param detector The detector
5555+ @param level Level threshold (0-255) *)
5656+val set_level_limit : t -> int -> unit
5757+5858+(** Set the minimum pulse width.
5959+6060+ Pulses shorter than this are ignored (noise filtering).
6161+6262+ @param detector The detector
6363+ @param samples Minimum width in samples *)
6464+val set_min_pulse_samples : t -> int -> unit
6565+6666+(** Set the maximum gap for end-of-package detection.
6767+6868+ @param detector The detector
6969+ @param ms Maximum gap in milliseconds *)
7070+val set_max_gap_ms : t -> float -> unit
7171+7272+(** {1 Statistics} *)
7373+7474+(** Get current signal level estimate.
7575+7676+ @param detector The detector
7777+ @return Current level estimate *)
7878+val get_level_estimate : t -> int
7979+8080+(** Get noise floor estimate.
8181+8282+ @param detector The detector
8383+ @return Noise floor estimate *)
8484+val get_noise_estimate : t -> int
8585+8686+(** Get detected center frequency (FSK mode).
8787+8888+ @param detector The detector
8989+ @return Center frequency offset, or 0 for OOK *)
9090+val get_center_frequency : t -> int
9191+9292+(** {1 Printing} *)
9393+9494+(** Pretty-print detector state. *)
9595+val pp : Format.formatter -> t -> unit
+144
lib/rtl433_pulse_slicer.ml
···11+(* Pulse slicing - convert pulse timing to bits *)
22+33+let pcm pulse_data ~short_width ~tolerance =
44+ let bits = Rtl433_bitbuffer.create () in
55+ let short_samples = Rtl433_pulse_data.us_to_samples pulse_data short_width in
66+ let tol_samples = Rtl433_pulse_data.us_to_samples pulse_data tolerance in
77+88+ for i = 0 to pulse_data.Rtl433_pulse_data.num_pulses - 1 do
99+ let pulse = pulse_data.Rtl433_pulse_data.pulses.(i) in
1010+ let gap = pulse_data.Rtl433_pulse_data.gaps.(i) in
1111+1212+ (* Count how many bit periods in the pulse *)
1313+ let pulse_bits = (pulse + (short_samples / 2)) / short_samples in
1414+ let gap_bits = (gap + (short_samples / 2)) / short_samples in
1515+1616+ ignore tol_samples;
1717+1818+ for _ = 1 to pulse_bits do
1919+ Rtl433_bitbuffer.add_bit bits 1
2020+ done;
2121+ for _ = 1 to gap_bits do
2222+ Rtl433_bitbuffer.add_bit bits 0
2323+ done
2424+ done;
2525+ bits
2626+2727+let pwm pulse_data ~short_width ~long_width ~tolerance ?sync_width () =
2828+ let bits = Rtl433_bitbuffer.create () in
2929+ let short_samples = Rtl433_pulse_data.us_to_samples pulse_data short_width in
3030+ let long_samples = Rtl433_pulse_data.us_to_samples pulse_data long_width in
3131+ let tol_samples = Rtl433_pulse_data.us_to_samples pulse_data tolerance in
3232+ let sync_samples =
3333+ match sync_width with
3434+ | Some sw -> Rtl433_pulse_data.us_to_samples pulse_data sw
3535+ | None -> 0
3636+ in
3737+3838+ for i = 0 to pulse_data.Rtl433_pulse_data.num_pulses - 1 do
3939+ let pulse = pulse_data.Rtl433_pulse_data.pulses.(i) in
4040+4141+ (* Skip sync pulses *)
4242+ if sync_samples > 0 && abs (pulse - sync_samples) < tol_samples then ()
4343+ else if abs (pulse - short_samples) < tol_samples then
4444+ Rtl433_bitbuffer.add_bit bits 1
4545+ else if abs (pulse - long_samples) < tol_samples then
4646+ Rtl433_bitbuffer.add_bit bits 0
4747+ (* else: timing error, skip *)
4848+ done;
4949+ bits
5050+5151+let ppm pulse_data ~short_width ~long_width ~tolerance =
5252+ let bits = Rtl433_bitbuffer.create () in
5353+ let short_samples = Rtl433_pulse_data.us_to_samples pulse_data short_width in
5454+ let long_samples = Rtl433_pulse_data.us_to_samples pulse_data long_width in
5555+ let tol_samples = Rtl433_pulse_data.us_to_samples pulse_data tolerance in
5656+5757+ for i = 0 to pulse_data.Rtl433_pulse_data.num_pulses - 1 do
5858+ let gap = pulse_data.Rtl433_pulse_data.gaps.(i) in
5959+6060+ if abs (gap - short_samples) < tol_samples then
6161+ Rtl433_bitbuffer.add_bit bits 0
6262+ else if abs (gap - long_samples) < tol_samples then
6363+ Rtl433_bitbuffer.add_bit bits 1
6464+ (* else: timing error or end marker *)
6565+ done;
6666+ bits
6767+6868+let manchester pulse_data ~short_width ~tolerance =
6969+ let bits = Rtl433_bitbuffer.create () in
7070+ let half_bit = Rtl433_pulse_data.us_to_samples pulse_data short_width in
7171+ let full_bit = half_bit * 2 in
7272+ let tol_samples = Rtl433_pulse_data.us_to_samples pulse_data tolerance in
7373+7474+ let state = ref 0 in (* 0 = expecting first half, 1 = expecting second *)
7575+7676+ for i = 0 to pulse_data.Rtl433_pulse_data.num_pulses - 1 do
7777+ let pulse = pulse_data.Rtl433_pulse_data.pulses.(i) in
7878+ let gap = pulse_data.Rtl433_pulse_data.gaps.(i) in
7979+8080+ (* Check pulse width *)
8181+ if abs (pulse - half_bit) < tol_samples then begin
8282+ if !state = 1 then begin
8383+ Rtl433_bitbuffer.add_bit bits 1;
8484+ state := 0
8585+ end
8686+ else state := 1
8787+ end
8888+ else if abs (pulse - full_bit) < tol_samples then begin
8989+ Rtl433_bitbuffer.add_bit bits 0;
9090+ state := 0
9191+ end;
9292+9393+ (* Check gap width *)
9494+ if abs (gap - half_bit) < tol_samples then begin
9595+ if !state = 1 then begin
9696+ Rtl433_bitbuffer.add_bit bits 0;
9797+ state := 0
9898+ end
9999+ else state := 1
100100+ end
101101+ else if abs (gap - full_bit) < tol_samples then begin
102102+ Rtl433_bitbuffer.add_bit bits 1;
103103+ state := 0
104104+ end
105105+ done;
106106+ bits
107107+108108+let dmc pulse_data ~short_width ~tolerance =
109109+ (* Differential Manchester - simplified *)
110110+ manchester pulse_data ~short_width ~tolerance
111111+112112+let slice pulse_data modulation ~short_width ~long_width ~tolerance ?sync_width () =
113113+ let open Rtl433_modulation in
114114+ match modulation with
115115+ | OOK Pulse_pcm | FSK Pulse_pcm ->
116116+ pcm pulse_data ~short_width ~tolerance
117117+ | OOK Pulse_pwm | FSK Pulse_pwm ->
118118+ pwm pulse_data ~short_width ~long_width ~tolerance ?sync_width ()
119119+ | OOK Pulse_ppm ->
120120+ ppm pulse_data ~short_width ~long_width ~tolerance
121121+ | OOK Pulse_manchester | FSK Pulse_manchester ->
122122+ manchester pulse_data ~short_width ~tolerance
123123+ | OOK Pulse_dmc ->
124124+ dmc pulse_data ~short_width ~tolerance
125125+ | OOK (Pulse_piwm_raw | Pulse_piwm_dc | Pulse_nrzs) ->
126126+ (* Fallback to PCM *)
127127+ pcm pulse_data ~short_width ~tolerance
128128+129129+let mark_row_boundaries pulse_data ~gap_limit =
130130+ ignore gap_limit;
131131+ pulse_data (* TODO: implement row splitting *)
132132+133133+let find_sync pulse_data ~sync_width ~tolerance =
134134+ let sync_samples = Rtl433_pulse_data.us_to_samples pulse_data sync_width in
135135+ let tol_samples = Rtl433_pulse_data.us_to_samples pulse_data tolerance in
136136+137137+ let rec find i =
138138+ if i >= pulse_data.Rtl433_pulse_data.num_pulses then None
139139+ else
140140+ let pulse = pulse_data.Rtl433_pulse_data.pulses.(i) in
141141+ if abs (pulse - sync_samples) < tol_samples then Some (i + 1)
142142+ else find (i + 1)
143143+ in
144144+ find 0
+138
lib/rtl433_pulse_slicer.mli
···11+(** Pulse slicing: convert pulse timing to bit sequences.
22+33+ This module converts pulse and gap timings into bits according
44+ to various encoding schemes (PCM, PWM, PPM, Manchester, etc.). *)
55+66+(** {1 Slicing Functions} *)
77+88+(** Slice pulses using PCM (NRZ) encoding.
99+1010+ Pulse = 1, No pulse = 0.
1111+ Each symbol period is {!short_width} microseconds.
1212+1313+ @param pulse_data Pulse timing data
1414+ @param short_width Symbol width in microseconds
1515+ @param tolerance Timing tolerance in microseconds
1616+ @return Bitbuffer with decoded bits *)
1717+val pcm :
1818+ Rtl433_pulse_data.t ->
1919+ short_width:float ->
2020+ tolerance:float ->
2121+ Rtl433_bitbuffer.t
2222+2323+(** Slice pulses using PWM encoding.
2424+2525+ Short pulse = 1, Long pulse = 0.
2626+ Gaps are ignored (fixed width expected).
2727+2828+ @param pulse_data Pulse timing data
2929+ @param short_width Short pulse width in microseconds
3030+ @param long_width Long pulse width in microseconds
3131+ @param tolerance Timing tolerance in microseconds
3232+ @param sync_width Optional sync pulse width (ignored if present)
3333+ @return Bitbuffer with decoded bits *)
3434+val pwm :
3535+ Rtl433_pulse_data.t ->
3636+ short_width:float ->
3737+ long_width:float ->
3838+ tolerance:float ->
3939+ ?sync_width:float ->
4040+ unit ->
4141+ Rtl433_bitbuffer.t
4242+4343+(** Slice pulses using PPM (Pulse Position Modulation) encoding.
4444+4545+ Short gap = 0, Long gap = 1.
4646+ Pulses are expected to be fixed width.
4747+4848+ @param pulse_data Pulse timing data
4949+ @param short_width Short gap width in microseconds
5050+ @param long_width Long gap width in microseconds
5151+ @param tolerance Timing tolerance in microseconds
5252+ @return Bitbuffer with decoded bits *)
5353+val ppm :
5454+ Rtl433_pulse_data.t ->
5555+ short_width:float ->
5656+ long_width:float ->
5757+ tolerance:float ->
5858+ Rtl433_bitbuffer.t
5959+6060+(** Slice pulses using Manchester encoding.
6161+6262+ Each bit period has a transition in the middle.
6363+ High-to-low transition = 0, Low-to-high transition = 1.
6464+6565+ @param pulse_data Pulse timing data
6666+ @param short_width Half-bit width in microseconds
6767+ @param tolerance Timing tolerance in microseconds
6868+ @return Bitbuffer with decoded bits *)
6969+val manchester :
7070+ Rtl433_pulse_data.t ->
7171+ short_width:float ->
7272+ tolerance:float ->
7373+ Rtl433_bitbuffer.t
7474+7575+(** Slice pulses using Differential Manchester encoding.
7676+7777+ Transition at bit boundary = 0, No transition = 1.
7878+7979+ @param pulse_data Pulse timing data
8080+ @param short_width Half-bit width in microseconds
8181+ @param tolerance Timing tolerance in microseconds
8282+ @return Bitbuffer with decoded bits *)
8383+val dmc :
8484+ Rtl433_pulse_data.t ->
8585+ short_width:float ->
8686+ tolerance:float ->
8787+ Rtl433_bitbuffer.t
8888+8989+(** {1 Generic Slicing} *)
9090+9191+(** Slice pulses according to device timing parameters.
9292+9393+ Automatically selects the appropriate slicer based on modulation type.
9494+9595+ @param pulse_data Pulse timing data
9696+ @param modulation Modulation type
9797+ @param short_width Short pulse width in microseconds
9898+ @param long_width Long pulse width in microseconds
9999+ @param tolerance Timing tolerance in microseconds
100100+ @param sync_width Optional sync pulse width
101101+ @return Bitbuffer with decoded bits *)
102102+val slice :
103103+ Rtl433_pulse_data.t ->
104104+ Rtl433_modulation.t ->
105105+ short_width:float ->
106106+ long_width:float ->
107107+ tolerance:float ->
108108+ ?sync_width:float ->
109109+ unit ->
110110+ Rtl433_bitbuffer.t
111111+112112+(** {1 Row Splitting} *)
113113+114114+(** Split pulse data into rows based on gap threshold.
115115+116116+ Long gaps cause a new row to be started in the bitbuffer.
117117+118118+ @param pulse_data Pulse timing data
119119+ @param gap_limit Gap threshold for new row in microseconds
120120+ @return Modified pulse data with row boundaries marked *)
121121+val mark_row_boundaries :
122122+ Rtl433_pulse_data.t ->
123123+ gap_limit:float ->
124124+ Rtl433_pulse_data.t
125125+126126+(** {1 Sync Detection} *)
127127+128128+(** Find sync pulse pattern in pulse data.
129129+130130+ @param pulse_data Pulse timing data
131131+ @param sync_width Expected sync pulse width in microseconds
132132+ @param tolerance Timing tolerance
133133+ @return Index of first pulse after sync, or None *)
134134+val find_sync :
135135+ Rtl433_pulse_data.t ->
136136+ sync_width:float ->
137137+ tolerance:float ->
138138+ int option
+114
lib/rtl433_registry.ml
···11+(* Decoder registry *)
22+33+type filter = All | Enable of int list | Disable of int list
44+55+type t = {
66+ decoders : unit Rtl433_device.t list;
77+ filter : filter;
88+}
99+1010+let create () = { decoders = []; filter = All }
1111+let register t decoder = { t with decoders = decoder :: t.decoders }
1212+let register_all t decoders = { t with decoders = decoders @ t.decoders }
1313+1414+(* Built-in decoders - registered externally to avoid circular deps *)
1515+let builtin_decoders_ref : (unit -> unit Rtl433_device.t list) ref = ref (fun () -> [])
1616+1717+let set_builtin_decoders_fn f = builtin_decoders_ref := f
1818+let builtin_decoders () = !builtin_decoders_ref ()
1919+let with_builtins () = { decoders = builtin_decoders (); filter = All }
2020+let apply_filter t filter = { t with filter }
2121+2222+let parse_filter specs =
2323+ let enabled = ref [] in
2424+ let disabled = ref [] in
2525+ List.iter
2626+ (fun s ->
2727+ match int_of_string_opt s with
2828+ | Some n when n < 0 -> disabled := -n :: !disabled
2929+ | Some n -> enabled := n :: !enabled
3030+ | None -> ())
3131+ specs;
3232+ match (!enabled, !disabled) with
3333+ | [], [] -> All
3434+ | [], ds -> Disable ds
3535+ | es, [] -> Enable es
3636+ | es, ds ->
3737+ (* Enable takes precedence *)
3838+ Enable (List.filter (fun e -> not (List.mem e ds)) es)
3939+4040+let is_enabled t decoder =
4141+ if decoder.Rtl433_device.disabled then false
4242+ else
4343+ match t.filter with
4444+ | All -> true
4545+ | Enable nums -> List.mem decoder.Rtl433_device.protocol_num nums
4646+ | Disable nums -> not (List.mem decoder.Rtl433_device.protocol_num nums)
4747+4848+let enabled t = List.filter (is_enabled t) t.decoders
4949+5050+let find_by_num t num =
5151+ List.find_opt (fun d -> d.Rtl433_device.protocol_num = num) t.decoders
5252+5353+let find_by_name t name =
5454+ let name_lower = String.lowercase_ascii name in
5555+ List.find_opt
5656+ (fun d -> String.lowercase_ascii d.Rtl433_device.name = name_lower)
5757+ t.decoders
5858+5959+let count t = List.length t.decoders
6060+let enabled_count t = List.length (enabled t)
6161+6262+let run_decoders t pulse_data ?(run_all = false) () =
6363+ let decoders = enabled t in
6464+ let results = ref [] in
6565+ let found_success = ref false in
6666+ List.iter
6767+ (fun d ->
6868+ if (not !found_success) || run_all then begin
6969+ let result = Rtl433_device.run d pulse_data in
7070+ results := (d, result) :: !results;
7171+ match result with Rtl433_device.Decoded _ -> found_success := true | _ -> ()
7272+ end)
7373+ decoders;
7474+ List.rev !results
7575+7676+let run_decoders_on_bits t modulation bits ?(run_all = false) () =
7777+ let decoders =
7878+ List.filter
7979+ (fun d -> d.Rtl433_device.modulation = modulation)
8080+ (enabled t)
8181+ in
8282+ let results = ref [] in
8383+ let found_success = ref false in
8484+ List.iter
8585+ (fun d ->
8686+ if (not !found_success) || run_all then begin
8787+ let result = Rtl433_device.run_on_bits d bits in
8888+ results := (d, result) :: !results;
8989+ match result with Rtl433_device.Decoded _ -> found_success := true | _ -> ()
9090+ end)
9191+ decoders;
9292+ List.rev !results
9393+9494+let get_stats _t = []
9595+let reset_stats _t = ()
9696+9797+let pp fmt t =
9898+ Format.fprintf fmt "Registry: %d decoders (%d enabled)@." (count t)
9999+ (enabled_count t);
100100+ List.iter
101101+ (fun d ->
102102+ let status = if is_enabled t d then "enabled" else "disabled" in
103103+ Format.fprintf fmt " %a [%s]@." Rtl433_device.pp d status)
104104+ t.decoders
105105+106106+let list_protocols t =
107107+ let buf = Buffer.create 256 in
108108+ List.iter
109109+ (fun d ->
110110+ Buffer.add_string buf
111111+ (Printf.sprintf "[%d] %s\n" d.Rtl433_device.protocol_num
112112+ d.Rtl433_device.name))
113113+ t.decoders;
114114+ Buffer.contents buf
+158
lib/rtl433_registry.mli
···11+(** Decoder registry for managing protocol decoders.
22+33+ This module maintains a collection of protocol decoders and handles
44+ protocol filtering (enable/disable). It provides functions to
55+ register decoders and run them against incoming pulse data. *)
66+77+(** {1 Protocol Filtering} *)
88+99+(** Protocol filter specification. *)
1010+type filter =
1111+ | All
1212+ (** Enable all protocols. *)
1313+ | Enable of int list
1414+ (** Enable only these protocol numbers. *)
1515+ | Disable of int list
1616+ (** Disable these protocol numbers (enable all others). *)
1717+1818+(** {1 Registry} *)
1919+2020+(** A decoder registry. *)
2121+type t
2222+2323+(** Create an empty registry. *)
2424+val create : unit -> t
2525+2626+(** {1 Registration} *)
2727+2828+(** Register a decoder.
2929+3030+ @param registry The registry
3131+ @param decoder Decoder to register
3232+ @return Updated registry *)
3333+val register : t -> unit Rtl433_device.t -> t
3434+3535+(** Register multiple decoders.
3636+3737+ @param registry The registry
3838+ @param decoders List of decoders to register
3939+ @return Updated registry *)
4040+val register_all : t -> unit Rtl433_device.t list -> t
4141+4242+(** {1 Built-in Decoders} *)
4343+4444+(** Set the function that provides built-in decoders.
4545+4646+ This is used to avoid circular dependencies between the registry
4747+ and decoder modules. Called during library initialization.
4848+4949+ @param f Function that returns the list of built-in decoders *)
5050+val set_builtin_decoders_fn : (unit -> unit Rtl433_device.t list) -> unit
5151+5252+(** Get all built-in decoders.
5353+5454+ Returns a list of all decoders compiled into the library. *)
5555+val builtin_decoders : unit -> unit Rtl433_device.t list
5656+5757+(** Create a registry with all built-in decoders. *)
5858+val with_builtins : unit -> t
5959+6060+(** {1 Filtering} *)
6161+6262+(** Apply a filter to the registry.
6363+6464+ @param registry The registry
6565+ @param filter Filter specification
6666+ @return Updated registry with filter applied *)
6767+val apply_filter : t -> filter -> t
6868+6969+(** Parse a protocol filter from strings.
7070+7171+ Positive numbers enable, negative numbers disable.
7272+ Example: ["142"; "-59"; "-60"] enables 142, disables 59 and 60.
7373+7474+ @param specs List of protocol number strings
7575+ @return Filter specification *)
7676+val parse_filter : string list -> filter
7777+7878+(** {1 Querying} *)
7979+8080+(** Get all enabled decoders.
8181+8282+ @param registry The registry
8383+ @return List of enabled decoders *)
8484+val enabled : t -> unit Rtl433_device.t list
8585+8686+(** Find a decoder by protocol number.
8787+8888+ @param registry The registry
8989+ @param num Protocol number
9090+ @return Decoder if found *)
9191+val find_by_num : t -> int -> unit Rtl433_device.t option
9292+9393+(** Find a decoder by name.
9494+9595+ @param registry The registry
9696+ @param name Protocol name (case-insensitive)
9797+ @return Decoder if found *)
9898+val find_by_name : t -> string -> unit Rtl433_device.t option
9999+100100+(** Get total count of registered decoders. *)
101101+val count : t -> int
102102+103103+(** Get count of enabled decoders. *)
104104+val enabled_count : t -> int
105105+106106+(** {1 Running Decoders} *)
107107+108108+(** Run all matching decoders on pulse data.
109109+110110+ Decoders are matched by modulation type and run in priority order.
111111+ Returns after the first successful decode, unless run_all is true.
112112+113113+ @param registry The registry
114114+ @param pulse_data Pulse timing data
115115+ @param run_all If true, run all decoders even after success
116116+ @return List of (decoder, result) pairs for decoders that were run *)
117117+val run_decoders :
118118+ t ->
119119+ Rtl433_pulse_data.t ->
120120+ ?run_all:bool ->
121121+ unit ->
122122+ (unit Rtl433_device.t * Rtl433_device.decode_result) list
123123+124124+(** Run all matching decoders on a bitbuffer.
125125+126126+ @param registry The registry
127127+ @param modulation Modulation type to filter by
128128+ @param bits Bitbuffer to decode
129129+ @param run_all If true, run all decoders even after success
130130+ @return List of (decoder, result) pairs *)
131131+val run_decoders_on_bits :
132132+ t ->
133133+ Rtl433_modulation.t ->
134134+ Rtl433_bitbuffer.t ->
135135+ ?run_all:bool ->
136136+ unit ->
137137+ (unit Rtl433_device.t * Rtl433_device.decode_result) list
138138+139139+(** {1 Statistics} *)
140140+141141+(** Get aggregate statistics for all decoders.
142142+143143+ @param registry The registry
144144+ @return Data structure with decoder statistics *)
145145+val get_stats : t -> Rtl433_data.t
146146+147147+(** Reset statistics for all decoders.
148148+149149+ @param registry The registry *)
150150+val reset_stats : t -> unit
151151+152152+(** {1 Printing} *)
153153+154154+(** Pretty-print registry contents. *)
155155+val pp : Format.formatter -> t -> unit
156156+157157+(** List all registered protocols. *)
158158+val list_protocols : t -> string
+129
lib/rtl433_util.ml
···11+(* Utility functions for rtl433 - CRC, bit manipulation, etc. *)
22+33+(* CRC-8 calculation with configurable polynomial and initial value *)
44+let crc8 ?(poly = 0x31) ?(init = 0x00) data len =
55+ let crc = ref init in
66+ for i = 0 to len - 1 do
77+ let byte = Bytes.get_uint8 data i in
88+ crc := !crc lxor byte;
99+ for _ = 0 to 7 do
1010+ if !crc land 0x80 <> 0 then
1111+ crc := ((!crc lsl 1) lxor poly) land 0xff
1212+ else
1313+ crc := (!crc lsl 1) land 0xff
1414+ done
1515+ done;
1616+ !crc
1717+1818+(* CRC-16 calculation with configurable polynomial and initial value *)
1919+let crc16 ?(poly = 0x8005) ?(init = 0x0000) data len =
2020+ let crc = ref init in
2121+ for i = 0 to len - 1 do
2222+ let byte = Bytes.get_uint8 data i in
2323+ crc := !crc lxor (byte lsl 8);
2424+ for _ = 0 to 7 do
2525+ if !crc land 0x8000 <> 0 then
2626+ crc := ((!crc lsl 1) lxor poly) land 0xffff
2727+ else
2828+ crc := (!crc lsl 1) land 0xffff
2929+ done
3030+ done;
3131+ !crc
3232+3333+(* XOR all bytes in a range *)
3434+let xor_bytes data offset len =
3535+ let result = ref 0 in
3636+ for i = offset to offset + len - 1 do
3737+ result := !result lxor (Bytes.get_uint8 data i)
3838+ done;
3939+ !result
4040+4141+(* Sum all bytes in data *)
4242+let add_bytes data len =
4343+ let sum = ref 0 in
4444+ for i = 0 to len - 1 do
4545+ sum := !sum + Bytes.get_uint8 data i
4646+ done;
4747+ !sum land 0xff
4848+4949+(* Get a single bit from byte array (MSB first) *)
5050+let get_bit data bit_idx =
5151+ let byte_idx = bit_idx / 8 in
5252+ let bit_pos = 7 - (bit_idx mod 8) in
5353+ if byte_idx < Bytes.length data then
5454+ (Bytes.get_uint8 data byte_idx lsr bit_pos) land 1
5555+ else
5656+ 0
5757+5858+(* Get a byte from bit offset (MSB first) *)
5959+let get_byte data ~bit_offset =
6060+ let byte_idx = bit_offset / 8 in
6161+ let bit_shift = bit_offset mod 8 in
6262+ if byte_idx >= Bytes.length data then 0
6363+ else if bit_shift = 0 then
6464+ Bytes.get_uint8 data byte_idx
6565+ else if byte_idx + 1 < Bytes.length data then
6666+ let hi = Bytes.get_uint8 data byte_idx in
6767+ let lo = Bytes.get_uint8 data (byte_idx + 1) in
6868+ ((hi lsl bit_shift) lor (lo lsr (8 - bit_shift))) land 0xff
6969+ else
7070+ (Bytes.get_uint8 data byte_idx lsl bit_shift) land 0xff
7171+7272+(* Get multiple bits from bit offset *)
7373+let get_bits data ~bit_offset ~count =
7474+ let result = ref 0 in
7575+ for i = 0 to count - 1 do
7676+ result := (!result lsl 1) lor get_bit data (bit_offset + i)
7777+ done;
7878+ !result
7979+8080+(* Reverse bits in a byte *)
8181+let reverse_byte b =
8282+ let b = ((b land 0xf0) lsr 4) lor ((b land 0x0f) lsl 4) in
8383+ let b = ((b land 0xcc) lsr 2) lor ((b land 0x33) lsl 2) in
8484+ ((b land 0xaa) lsr 1) lor ((b land 0x55) lsl 1)
8585+8686+(* Calculate parity (1 if odd number of 1s, 0 if even) *)
8787+let parity8 b =
8888+ let b = b lxor (b lsr 4) in
8989+ let b = b lxor (b lsr 2) in
9090+ let b = b lxor (b lsr 1) in
9191+ b land 1
9292+9393+(* Reflect bits (reverse bit order within width bits) *)
9494+let reflect data width =
9595+ let result = ref 0 in
9696+ for i = 0 to width - 1 do
9797+ if data land (1 lsl i) <> 0 then
9898+ result := !result lor (1 lsl (width - 1 - i))
9999+ done;
100100+ !result
101101+102102+(* Swap nibbles in a byte *)
103103+let swap_nibbles b =
104104+ ((b land 0x0f) lsl 4) lor ((b land 0xf0) lsr 4)
105105+106106+(* Convert bytes to hex string *)
107107+let bytes_to_hex data len =
108108+ let hex = Buffer.create (len * 2) in
109109+ for i = 0 to len - 1 do
110110+ Buffer.add_string hex (Printf.sprintf "%02x" (Bytes.get_uint8 data i))
111111+ done;
112112+ Buffer.contents hex
113113+114114+(* Convert hex string to bytes *)
115115+let hex_to_bytes hex =
116116+ let len = String.length hex / 2 in
117117+ let data = Bytes.create len in
118118+ for i = 0 to len - 1 do
119119+ let hi = Char.code hex.[i * 2] in
120120+ let lo = Char.code hex.[i * 2 + 1] in
121121+ let hex_val c =
122122+ if c >= Char.code '0' && c <= Char.code '9' then c - Char.code '0'
123123+ else if c >= Char.code 'a' && c <= Char.code 'f' then c - Char.code 'a' + 10
124124+ else if c >= Char.code 'A' && c <= Char.code 'F' then c - Char.code 'A' + 10
125125+ else 0
126126+ in
127127+ Bytes.set_uint8 data i ((hex_val hi lsl 4) lor hex_val lo)
128128+ done;
129129+ data
+105
lib/rtl433_util.mli
···11+(** Utility functions for bit manipulation and checksums.
22+33+ This module provides low-level utilities for working with binary data,
44+ including CRC calculations, bit extraction, and byte manipulation. *)
55+66+(** {1 CRC Calculations} *)
77+88+(** Calculate CRC-8 checksum.
99+1010+ @param poly Polynomial (default 0x31, used by many sensors)
1111+ @param init Initial value (default 0x00)
1212+ @param data Input bytes
1313+ @param len Number of bytes to process
1414+ @return CRC-8 checksum *)
1515+val crc8 : ?poly:int -> ?init:int -> bytes -> int -> int
1616+1717+(** Calculate CRC-16 checksum.
1818+1919+ @param poly Polynomial (default 0x8005)
2020+ @param init Initial value (default 0x0000)
2121+ @param data Input bytes
2222+ @param len Number of bytes to process
2323+ @return CRC-16 checksum *)
2424+val crc16 : ?poly:int -> ?init:int -> bytes -> int -> int
2525+2626+(** XOR all bytes together.
2727+2828+ @param data Input bytes
2929+ @param offset Starting offset
3030+ @param len Number of bytes to XOR
3131+ @return XOR of all bytes *)
3232+val xor_bytes : bytes -> int -> int -> int
3333+3434+(** Add all bytes together (modulo 256).
3535+3636+ @param data Input bytes
3737+ @param len Number of bytes to add
3838+ @return Sum of bytes mod 256 *)
3939+val add_bytes : bytes -> int -> int
4040+4141+(** {1 Bit Manipulation} *)
4242+4343+(** Get a single bit from a byte array.
4444+4545+ @param data Byte array
4646+ @param bit_idx Bit index (0 = MSB of first byte)
4747+ @return 0 or 1 *)
4848+val get_bit : bytes -> int -> int
4949+5050+(** Get a byte from a potentially unaligned bit position.
5151+5252+ @param data Byte array
5353+ @param bit_offset Bit offset to start from
5454+ @return Byte value *)
5555+val get_byte : bytes -> bit_offset:int -> int
5656+5757+(** Extract multiple bits as an integer.
5858+5959+ @param data Byte array
6060+ @param bit_offset Starting bit offset
6161+ @param count Number of bits to extract (max 32)
6262+ @return Integer value *)
6363+val get_bits : bytes -> bit_offset:int -> count:int -> int
6464+6565+(** Reverse the bits in a byte.
6666+6767+ @param b Byte value (0-255)
6868+ @return Reversed byte *)
6969+val reverse_byte : int -> int
7070+7171+(** Calculate parity of a byte.
7272+7373+ @param b Byte value
7474+ @return 0 for even parity, 1 for odd *)
7575+val parity8 : int -> int
7676+7777+(** {1 Byte Operations} *)
7878+7979+(** Reflect (reverse) bits in a value.
8080+8181+ @param data Value to reflect
8282+ @param width Number of bits to reflect
8383+ @return Reflected value *)
8484+val reflect : int -> int -> int
8585+8686+(** Swap nibbles in a byte.
8787+8888+ @param b Byte value
8989+ @return Byte with nibbles swapped *)
9090+val swap_nibbles : int -> int
9191+9292+(** {1 String Formatting} *)
9393+9494+(** Format bytes as hex string.
9595+9696+ @param data Byte array
9797+ @param len Number of bytes
9898+ @return Hex string like "0a1b2c" *)
9999+val bytes_to_hex : bytes -> int -> string
100100+101101+(** Parse hex string to bytes.
102102+103103+ @param hex Hex string
104104+ @return Byte array *)
105105+val hex_to_bytes : string -> bytes
+41
rtl433.opam
···11+# This file is generated by dune, edit dune-project instead
22+opam-version: "2.0"
33+synopsis: "RTL-433 decoder for wireless sensors in OCaml"
44+description: """
55+Pure OCaml implementation of rtl_433, a software decoder for wireless sensors
66+ transmitting in the 433MHz/868MHz/915MHz ISM bands. Supports FSK and OOK
77+ modulation with pluggable protocol decoders and MQTT output."""
88+maintainer: ["Anil Madhavapeddy"]
99+authors: ["Anil Madhavapeddy"]
1010+license: "ISC"
1111+tags: ["sdr" "rtl-sdr" "433mhz" "ism" "mqtt" "sensors"]
1212+homepage: "https://github.com/avsm/ocaml-rtl433"
1313+doc: "https://avsm.github.io/ocaml-rtl433"
1414+bug-reports: "https://github.com/avsm/ocaml-rtl433/issues"
1515+depends: [
1616+ "dune" {>= "3.0"}
1717+ "ocaml" {>= "5.1"}
1818+ "eio" {>= "1.0"}
1919+ "eio_main"
2020+ "mqtte" {>= "0.1"}
2121+ "cmdliner" {>= "1.2"}
2222+ "fmt" {>= "0.9"}
2323+ "logs" {>= "0.7"}
2424+ "ptime"
2525+ "odoc" {with-doc}
2626+]
2727+build: [
2828+ ["dune" "subst"] {dev}
2929+ [
3030+ "dune"
3131+ "build"
3232+ "-p"
3333+ name
3434+ "-j"
3535+ jobs
3636+ "@install"
3737+ "@runtest" {with-test}
3838+ "@doc" {with-doc}
3939+ ]
4040+]
4141+dev-repo: "git+https://github.com/avsm/ocaml-rtl433.git"