Add advanced burst support.
M ant-wireless.gemspec +3 -3
@@ 1,16 1,16 @@ 
 # -*- encoding: utf-8 -*-
-# stub: ant-wireless 0.3.0.pre.20211011092718 ruby lib
+# stub: ant-wireless 0.4.0.pre.20211028134625 ruby lib
 # stub: ext/ant_ext/extconf.rb
 
 Gem::Specification.new do |s|
   s.name = "ant-wireless".freeze
-  s.version = "0.3.0.pre.20211011092718"
+  s.version = "0.4.0.pre.20211028134625"
 
   s.required_rubygems_version = Gem::Requirement.new("> 1.3.1".freeze) if s.respond_to? :required_rubygems_version=
   s.metadata = { "bug_tracker_uri" => "https://todo.sr.ht/~ged/ruby-ant-wireless", "changelog_uri" => "https://deveiate.org/code/ant-wireless/History_md.html", "documentation_uri" => "https://deveiate.org/code/ant-wireless", "homepage_uri" => "https://sr.ht/~ged/ruby-ant-wireless/", "source_uri" => "https://hg.sr.ht/~ged/ruby-ant-wireless" } if s.respond_to? :metadata=
   s.require_paths = ["lib".freeze]
   s.authors = ["Michael Granger".freeze, "Mahlon E. Smith".freeze]
-  s.date = "2021-10-11"
+  s.date = "2021-10-28"
   s.description = "A binding for the ANT ultra-low power wireless protocol via the Garmin USB ANT Stick. ANT can be used to send information wirelessly from one device to another device, in a robust and flexible manner.".freeze
   s.email = ["ged@FaerieMUD.org".freeze, "mahlon@martini.nu".freeze]
   s.extensions = ["ext/ant_ext/extconf.rb".freeze]

          
M ext/ant_ext/ant_ext.c +83 -3
@@ 225,6 225,15 @@ rant_s_init( int argc, VALUE *argv, VALU
 }
 
 
+/*
+ * call-seq:
+ *    Ant.initialized?   -> true or false
+ *
+ * Returns +true+ if the ANT library has been initialized.
+ *
+ * Note: this requires a modified version of the Garmin ANT-SDK.
+ *
+ */
 static VALUE
 rant_s_initialized_p( VALUE _module )
 {

          
@@ 418,6 427,56 @@ rant_s_use_extended_messages_eq( VALUE _
 }
 
 
+/*
+ * call-seq:
+ *    Ant.configure_advanced_burst( enabled, max_packet_length, required_fields, optional_fields,
+ *        stall_count=3210, retry_count=4 )
+ *
+ * Enable/disable and configure advanced burst. This is the lower-level method; the
+ * higher-level methods are: #enable_advanced_burst and #disable_advanced_burst.
+ *
+ */
+static VALUE
+rant_s_configure_advanced_burst( int argc, VALUE *argv, VALUE _module )
+{
+	VALUE enabled,
+		max_packet_length,
+		required_fields,
+		optional_fields,
+		stall_count = Qnil,
+		retry_count = Qnil;
+	bool bEnable;
+	unsigned char ucMaxPacketLength,
+		ucRetryCount = 0;
+	unsigned long ulRequiredFields,
+		ulOptionalFields;
+	unsigned short usStallCount = 0;
+	bool rval;
+
+	rb_scan_args( argc, argv, "42", &enabled, &max_packet_length, &required_fields,
+		&optional_fields, &stall_count, &retry_count );
+
+	bEnable = RTEST( enabled );
+	ucMaxPacketLength = NUM2CHR( max_packet_length );
+	ulRequiredFields = NUM2ULONG( required_fields );
+	ulOptionalFields = NUM2ULONG( optional_fields );
+
+	if ( RTEST(stall_count) ) {
+		usStallCount = NUM2USHORT( stall_count );
+	}
+	if ( RTEST(retry_count) ) {
+		ucRetryCount = NUM2CHR( retry_count );
+	}
+
+	rant_log( "warn", "Configuring advanced burst: enable = %d, maxpacketlength = %d",
+		bEnable, ucMaxPacketLength );
+	rval = ANT_ConfigureAdvancedBurst_ext( bEnable, ucMaxPacketLength, ulRequiredFields,
+		ulOptionalFields, usStallCount, ucRetryCount );
+
+	return rval ? Qtrue : Qfalse;
+}
+
+
 // Buffer for response data.
 // static UCHAR pucResponseBuffer[ MESG_RESPONSE_EVENT_SIZE ];
 static UCHAR pucResponseBuffer[ MESG_MAX_SIZE_VALUE ];

          
@@ 510,7 569,7 @@ rant_s_on_response( int argc, VALUE *arg
  *
  */
 static VALUE
-rant_s_request_capabilities( VALUE module )
+rant_s_request_capabilities( VALUE _module )
 {
 	bool rval = ANT_RequestMessage( 0, MESG_CAPABILITIES_ID );
 	return rval ? Qtrue : Qfalse;

          
@@ 527,7 586,7 @@ rant_s_request_capabilities( VALUE modul
  *
  */
 static VALUE
-rant_s_request_serial_num( VALUE module )
+rant_s_request_serial_num( VALUE _module )
 {
 	bool rval = ANT_RequestMessage( 0, MESG_GET_SERIAL_NUM_ID );
 	return rval ? Qtrue : Qfalse;

          
@@ 544,7 603,7 @@ rant_s_request_serial_num( VALUE module 
  *
  */
 static VALUE
-rant_s_request_version( VALUE module )
+rant_s_request_version( VALUE _module )
 {
 	bool rval = ANT_RequestMessage( 0, MESG_VERSION_ID );
 	return rval ? Qtrue : Qfalse;

          
@@ 553,6 612,23 @@ rant_s_request_version( VALUE module )
 
 /*
  * call-seq:
+ *    Ant.request_advanced_burst_capabilities
+ *
+ * Request the current device's advanced burst capabilities. The result will
+ * be delivered via a callback to the #on_version response callback, which by
+ * default extracts it and stores it at Ant.advanced_burst_capabilities.
+ *
+ */
+static VALUE
+rant_s_request_advanced_burst_capabilities( VALUE _module )
+{
+	bool rval = ANT_RequestMessage( 0, MESG_CONFIG_ADV_BURST_ID );
+	return rval ? Qtrue : Qfalse;
+}
+
+
+/*
+ * call-seq:
  *    Ant.log_directory = "path/to/log/dir"
  *
  * Write debugging logs to the specified directory, which should already exist.

          
@@ 606,6 682,8 @@ Init_ant_ext()
 
 	rb_define_singleton_method( rant_mAnt, "use_extended_messages=",
 		rant_s_use_extended_messages_eq, 1 );
+	rb_define_singleton_method( rant_mAnt, "configure_advanced_burst",
+		rant_s_configure_advanced_burst, -1 );
 
 	rb_define_singleton_method( rant_mAnt, "on_response", rant_s_on_response, -1 );
 	// EXPORT void ANT_UnassignAllResponseFunctions(); //Unassigns all response functions

          
@@ 613,6 691,8 @@ Init_ant_ext()
 	rb_define_singleton_method( rant_mAnt, "request_capabilities", rant_s_request_capabilities, 0 );
 	rb_define_singleton_method( rant_mAnt, "request_serial_num", rant_s_request_serial_num, 0 );
 	rb_define_singleton_method( rant_mAnt, "request_version", rant_s_request_version, 0 );
+	rb_define_singleton_method( rant_mAnt, "request_advanced_burst_capabilities",
+		rant_s_request_advanced_burst_capabilities, 0 );
 
 	rb_define_singleton_method( rant_mAnt, "log_directory=", rant_s_log_directory_eq, 1 );
 

          
M ext/ant_ext/channel.c +59 -0
@@ 9,6 9,8 @@ 
 
 #include "ant_ext.h"
 
+#define DEFAULT_ADV_PACKETS 3
+
 VALUE rant_cAntChannel;
 
 VALUE rant_mAntDataUtilities;

          
@@ 300,6 302,14 @@ rant_channel_set_channel_rf_freq( VALUE 
 }
 
 
+/*
+ * call-seq:
+ *    channel.set_frequency_agility( freq1, freq2, freq3 )
+ *
+ * Set the frequencies to use in frequency agility (integers between 0 and 124). These values
+ * use the same convention as the +rf_frequency+ setting; i.e., they're offsets from 2400 MHz.
+ *
+ */
 static VALUE
 rant_channet_set_frequency_agility( VALUE self, VALUE freq1, VALUE freq2, VALUE freq3 )
 {

          
@@ 582,6 592,54 @@ rant_channel_send_broadcast_data( VALUE 
 }
 
 
+/*
+ * call-seq:
+ *    channel.send_advanced_transfer( data, packets_per_message=3 )
+ *
+ * Send the given +data+ as one or more advanced burst packets. The +packets_per_message+
+ * may be set to a value between 1 and 3 to control how many 8-byte packets are send with
+ * each message.
+ *
+ */
+static VALUE
+rant_channel_send_advanced_transfer( int argc, VALUE *argv, VALUE self )
+{
+	rant_channel_t *ptr = rant_get_channel( self );
+	VALUE data = Qnil, packets = Qnil;
+	unsigned char *data_s;
+	long data_len;
+	unsigned short usNumDataPackets = data_len / 8,
+		remainingBytes = data_len % 8;
+	unsigned char ucStdPcktsPerSerialMsg = DEFAULT_ADV_PACKETS;
+
+	rb_scan_args( argc, argv, "11", &data, &packets );
+	data_len = RSTRING_LEN( data );
+	if ( RTEST(packets) ) {
+		ucStdPcktsPerSerialMsg = NUM2CHR(packets);
+	}
+
+	data_s = ALLOC_N( unsigned char, data_len );
+	strncpy( (char *)data_s, StringValuePtr(data), data_len );
+
+	// Pad it to 8-byte alignment
+	if ( remainingBytes ) {
+		int pad_bytes = (8 - remainingBytes);
+		REALLOC_N( data_s, unsigned char, data_len + pad_bytes );
+		memset( data_s + data_len, 0, pad_bytes );
+
+		usNumDataPackets += 1;
+	}
+
+	rant_log_obj( self, "warn", "Sending advanced burst packets (%d-byte messages)",
+		ucStdPcktsPerSerialMsg * 8 );
+	if ( !ANT_SendAdvancedBurstTransfer(ptr->channel_num, data_s, usNumDataPackets, ucStdPcktsPerSerialMsg) ) {
+		rb_raise( rb_eRuntimeError, "failed to send advanced burst transfer." );
+	}
+
+	return Qtrue;
+}
+
+
 void
 init_ant_channel()
 {

          
@@ 630,6 688,7 @@ init_ant_channel()
 	rb_define_method( rant_cAntChannel, "send_burst_transfer", rant_channel_send_burst_transfer, 1 );
 	rb_define_method( rant_cAntChannel, "send_acknowledged_data", rant_channel_send_acknowledged_data, 1 );
 	rb_define_method( rant_cAntChannel, "send_broadcast_data", rant_channel_send_broadcast_data, 1 );
+	rb_define_method( rant_cAntChannel, "send_advanced_transfer", rant_channel_send_advanced_transfer, -1 );
 
 	rb_define_method( rant_cAntChannel, "on_event", rant_channel_on_event, -1 );
 

          
M ext/ant_ext/extconf.rb +5 -0
@@ 18,6 18,11 @@ have_func( 'ANT_IsInitialized', 'libant.
 have_func( 'ANT_LibVersion', 'libant.h' )
 have_func( 'ANT_GetDeviceSerialNumber', 'libant.h' )
 
+# Ref: https://bugs.ruby-lang.org/issues/17865
+$CPPFLAGS << " -Wno-compound-token-split-by-macro "
+
+$CFLAGS << " -g "
+
 create_header()
 create_makefile( 'ant_ext' )
 

          
M lib/ant.rb +58 -0
@@ 26,6 26,14 @@ module Ant
 	# The valid offsets for the "RF Frequency" setting; this is an offset from 2400Hz.
 	VALID_RF_FREQUENCIES = ( 0...124 ).freeze
 
+	# Default options for advanced burst when it's enabled.
+	DEFAULT_ADVANCED_OPTIONS = {
+		max_packet_length: 24,
+		frequency_hopping: :optional,
+		stall_count: 0,
+		retry_count: 0
+	}
+
 
 	# Loggability API -- set up a logger for the library
 	log_as :ant

          
@@ 143,5 151,55 @@ module Ant
 		return offset
 	end
 
+
+	### Enable advanced burst mode with the given +options+.
+	def self::enable_advanced_burst( **options )
+		options = DEFAULT_ADVANCED_OPTIONS.merge( options )
+
+		max_packet_length = self.convert_max_packet_length( options[:max_packet_length] )
+
+		required_fields = self.make_required_fields_config( options )
+		optional_fields = self.make_optional_fields_config( options )
+
+		stall_count = options[:stall_count]
+		retry_count = options[:retry_count]
+
+		self.configure_advanced_burst( true, max_packet_length, required_fields, optional_fields,
+			stall_count, retry_count )
+	end
+
+
+	### Validate that the specified +length+ (in bytes) is a valid setting as an
+	### advanced burst max packet length configuration value. Returns the equivalent
+	### configuration value.
+	def self::convert_max_packet_length( length )
+		case length
+		when 8 then return 0x01
+		when 16 then return 0x02
+		when 24 then return 0x03
+		else
+			raise ArgumentError,
+				"invalid max packet length; expected 8, 16, or 24, got %p" % [ length ]
+		end
+	end
+
+
+	### Given an options hash, return a configuration value for required fields.
+	def self::make_required_fields_config( **options )
+		value = 0
+		value |= 0x01 if options[:frequency_hopping] == :required
+
+		return value
+	end
+
+
+	### Given an options hash, return a configuration value for optional fields.
+	def self::make_optional_fields_config( **options )
+		value = 0
+		value |= 0x01 if options[:frequency_hopping] == :optional
+
+		return value
+	end
+
 end # module Ant
 

          
M lib/ant/response_callbacks.rb +52 -2
@@ 9,7 9,8 @@ require 'ant/bitvector'
 
 # A module that handles response callbacks by logging them.
 module Ant::ResponseCallbacks
-	extend Loggability
+	extend Loggability,
+		Ant::DataUtilities
 
 	# Loggability API -- send logs to the Ant logger
 	log_to :ant

          
@@ 49,9 50,10 @@ module Ant::ResponseCallbacks
 		Ant::Message::MESG_RADIO_TX_POWER_ID            => :on_radio_tx_power,
 
 		Ant::Message::MESG_AUTO_FREQ_CONFIG_ID          => :on_auto_freq_config,
+		Ant::Message::MESG_CONFIG_ADV_BURST_ID          => :on_config_adv_burst_id,
 
 		# :TODO: There are many other MESG_ constants, but I think most or all of
-		# them are for the serial protocol.
+		# them are only used for the serial protocol.
 	}
 
 

          
@@ 236,6 238,54 @@ module Ant::ResponseCallbacks
 	end
 
 
+	### Handle callback when requesting advanced burst configuration.
+	def on_config_adv_burst_id( type, data )
+		self.log.debug "Advanced burst config/capabilities: 0x%02x: %p" % [ type, data ]
+
+		# Advanced burst capabilities
+		if type == 0
+			max_packet_length, features = data.unpack( 'CV' )
+			features = Ant::BitVector.new( features )
+
+			caps = {
+				max_packet_length: max_packet_length,
+				frequency_hopping: features.on?( Ant::ADV_BURST_CONFIG_FREQ_HOP )
+			}
+
+			self.log.info "Advanced burst capabilities: %p" % [ caps ]
+			Ant.instance_variable_set( :@advanced_burst_capabilities, caps );
+
+		# Advanced burst current configuration
+		elsif type == 1
+			enabled, max_packet_length, required, optional, stall_count, retry_count =
+				data.unpack( 'CCVVvC' )
+			required = Ant::BitVector.new( required )
+			optional = Ant::BitVector.new( optional )
+
+			required_features = []
+			required_features << :frequency_hopping if required.on?( Ant::ADV_BURST_CONFIG_FREQ_HOP )
+
+			optional_features = []
+			optional_features << :frequency_hopping if optional.on?( Ant::ADV_BURST_CONFIG_FREQ_HOP )
+
+			config = {
+				enabled: enabled == 1,
+				max_packet_length: max_packet_length,
+				required_features: required_features,
+				optional_features: optional_features,
+				stall_count: stall_count,
+				retry_count_extension: retry_count
+			}
+
+			self.log.info "Advanced burst configuration: %p" % [ config ]
+			Ant.instance_variable_set( :@advanced_burst_config, config );
+
+		else
+			self.log.warn "Unknown advanced burst response type %p." % [ type ]
+		end
+	end
+
+
 	### Handle capabilities response event.
 	def on_capabilities( channel_num, data )
 		std_opts  = Ant::BitVector.new( data.bytes[2] )