# frozen_string_literal: true module Vissen module Input module Message # This is the base message implementaion. This class should never be used # directly, but rather be subclassed to create the various messages that # the system understands. # # The Base class keeps track of subclasses and can produce a message # factory for all the implementations that it knows about (see # `.factory`). class Base include Message # @see Message DATA_LENGTH = 3 # @see Message STATUS = 0 # Checks message data consistency with the class default matcher. # # @return [true, false] true if the message data matches the class # matcher. def valid? self.class.matcher.match? data end class << self extend Forwardable # rubocop:disable Metrics/AbcSize # Returns a new instance of a Matcher, configured to match this # particular Message class. Subclasses of Base can utilize the same # functionality by simply redefining STATUS and, if necessary, # STATUS_MASK. # # By supplying the optional named arguments channel and number # # Raises a RangeError if the channel is given and is outside its valid # range of (0..15). # Raises a RangeError if number is given and is outside its valid # range of (0..127). # # @param channel [nil, Integer] the channel to match, or nil to match # all channels. # @param number [nil, Integer] the second byte value to match, or nil # to match all values. # @return [Matcher] the matcher that fulfills the requirements. def matcher(channel: nil, number: nil) return klass_matcher unless channel || number val, mask = status_value_and_mask channel if number raise RangeError unless (0...128).cover? number Matcher.new(self) { |d| (d[0] & mask) == val && d[1] == number } else Matcher.new(self) { |d| (d[0] & mask) == val } end end # rubocop:enable Metrics/AbcSize # @!method match?(obj) # Accessor to `Matcher#match?` on the default class matcher. # # @see Matcher#match? # # @param obj [#to_a] the message or data to match. # @return [true, false] (see Matcher#match?) def_delegator :klass_matcher, :match?, :match? # @!method match(obj) # Accessor to `Matcher#match` on the default class matcher. # # @see Matcher#match # # @param obj [Hash, Message] the message to match. # @return [false, Object, Message] (see Matcher#match) def_delegator :klass_matcher, :match, :match # Creates a new factory with all the subclasses of base added to it as # matchers. # # @return [MessageFactory] a factory configured to build all # subclasses of Base. def factory raise RuntimeError unless defined? @subclasses MessageFactory.new @subclasses.map(&:matcher) end # Alias to `#matcher` that swaps named arguments for positional ones. # # @see #matcher # # @param (see #matcher) # @return (see #matcher) def [](channel, number = nil) matcher channel: channel, number: number end # Build a new instance of `Message::Base`, or a subclass, using more # intuitive arguments. Subclasses of Base can utilize the same # functionality by simply redefining `DATA_LENGTH` to correspond to # their message length. # # Note that status and channel are masked using the default masks, and # not the constants that may have been defined by a subclass. # # @param bytes [Array] the message data byte values. # Unspecified values default to 0. # @param status [Integer] the status to use for the new message. # @param channel [Integer] the channel to use for the new message. # @param timestamp [Float] the timestamp to use for the new message. # @return [Base] a new instance of this class. def create(*bytes, status: self::STATUS, channel: 0, timestamp: Time.now.to_f) raise ArgumentError if bytes.length >= self::DATA_LENGTH validate_status status validate_channel channel data = Array.new self::DATA_LENGTH, 0 # Note: this line line must reference # STATUS_MASK and not self::STATUS_MASK data[0] = (status & STATUS_MASK) + (channel & CHANNEL_MASK) # Copy the bytes bytes.each_with_index { |value, index| data[index + 1] = value } new data, timestamp end protected # Called automatically by inheriting classes. # # @param subclass [Base] the inheriting class. # @return [nil] def inherited(subclass) (@subclasses ||= []) << subclass nil end # Helper method to validate a status value. # # @raise [RangeError] if the status is outside its allowable range. # # @param status [Integer] the status to validate. # @return [true] when the status is valid. def validate_status(status) raise RangeError unless (status & ~STATUS_MASK).zero? true end # Helper method to validate a channel value. # # @raise [RangeError] if the channel is outside its allowable range. # # @param channel [Integer] the channel to validate. # @return [true] when the channel is valid. def validate_channel(channel) raise RangeError unless (channel & ~CHANNEL_MASK).zero? true end # Creates a value and mask that can be used to match the first byte of # a message. # # == Usage # The following example illustrates how the value and mask are # intended to be used. # # value, mask = status_value_and_mask # 0x42 & mask == value # => true if STATUS == 0x40 # # value, mask = status_value_and_mask 3 # 0x42 & mask == value # => false since channel is 2 # # @param channel [Integer] the channel to match. # @return [Array] a value and a mask. def status_value_and_mask(channel = nil) if channel validate_channel channel [(self::STATUS & self::STATUS_MASK) + (channel & CHANNEL_MASK), self::STATUS_MASK + CHANNEL_MASK] else [self::STATUS, self::STATUS_MASK] end end # The klass matcher is the most generic `Matcher` for the message # class and is cached to avoid duplication. By default the default # `Matcher` uses the value and mask returned by # `#status_value_and_mask` to match messages. Subclasses that need # different behaviour can pass a block to be forwarded directly to the # matcher (see Matcher.new). # # @param block [Proc] the block that should be passed to # `Matcher.new` when first creating the matcher. # @return [Matcher] a matcher that matches all messages of this type. def klass_matcher(&block) return @klass_matcher if defined?(@klass_matcher) unless block_given? val, mask = status_value_and_mask block = proc { |d| (d[0] & mask) == val } end @klass_matcher = Matcher.new(self, &block) end end end end end end