Dave Jaffe
Published © Apache-2.0

FM Radio Data System Scanner

A Raspberry Pi equipped with 2 FM radios, one plays the station and the other scans the dial and shows the artist and title on each station

IntermediateWork in progressOver 1 day2,752
FM Radio Data System Scanner

Things used in this project

Hardware components

SparkFun FM Tuner Evaluation Board
×1
SparkFun FM Tuner Basic Breakout - Si4703
SparkFun FM Tuner Basic Breakout - Si4703
×1
Raspberry Pi 2 Model B
Raspberry Pi 2 Model B
×1
Adafruit Pi Box Plus
×1

Software apps and online services

Python for Raspberry Pi

Story

Read more

Code

si4703_play_one_command_line.py

Python
si4703_play_one_command_line.py: reads specified frequency and volume from command line, plays station, exits without powering down
Syntax: python si4703_play_one_command_line.py ic2_bus frequency(MHz) volume
# Connects Silicon Labs Si4703 radio receiver (from Sparkfun kit WRL-12938) with Raspberry Pi 2 Model B v1.1
# si4703_play_one_command_line.py: reads specified frequency and volume from command line, plays station, exits without powering down
# Syntax: python si4703_play_one_command_line.py i2c_bus frequency(MHz) volume
# Dave Jaffe
# January 2021

# Silicon Labs Documentation:
# Operating instructions: https://www.silabs.com/documents/public/application-notes/AN230.pdf (AN230)
# Registers: https://www.silabs.com/documents/public/data-sheets/Si4702-03-C19.pdf (Si4702-03-C19)

# Register read/write information from KansasCoder (see https://www.raspberrypi.org/forums/viewtopic.php?t=28920)
# Initialization based on Pi Flyer in thread at 
#   https://www.raspberrypi.org/forums/viewtopic.php?f=41&t=28920&start=25&sid=eb677d15d2577540ab9e7f68ce76b35a

import sys
import RPi.GPIO as GPIO
import smbus
from time import time, sleep
import subprocess
from os import path

def main():  
  # Check correct number of arguments, read them
  if (len(sys.argv) != 4): 
    print("Syntax: python si4703_play_one_command_line.py i2c_bus frequency(MHz) volume")
    exit(-1)
  else:
    i2c_bus = int(sys.argv[1])
    freq = float(sys.argv[2])
    volume = int(sys.argv[3])
    print("Playing radio on i2c bus %d with frequency %5.1f and volume %d" % (i2c_bus, freq, volume))
  
  initialize_si4703(i2c_bus)
  i2c = smbus.SMBus(i2c_bus)
  address = 0x10 #address of SI4703
  
  # Si4703 contains 32 8-bit integers, each corresponding to a byte of a 16-bit word:
  # reg = i2c.read_i2c_block_data(address, value, 32) returns them as:
  # 0A high, 0A low ... 0F high, 0F low, 00 high, 00 low .. 09 high, 09 low
  # We will combine high and low bytes since Silicon Labs register spec refers to entire 16-bit word
  # The mapping between a register number reg_no and the location of its high byte in reg is
  # ind = 2 * ((16 + (reg_no - 0xA)) % 16)

  # NOTE about register reads: 
  # i2c.read_i2c_block_data(address, value, 32) also WRITES value to Reg 2 high
  # as discovered by KansasCoder (see https://www.raspberrypi.org/forums/viewtopic.php?t=28920)
  # Thus we must always keep track of Reg 2 high, and then send it as i2c.read_i2c_block_data 2nd parameter
  # We will define 
  # shadow_register = reg[16:28]   # Reg 2 high, 2 low, ... 7 high, 7 low (only Reg 2 - 7 are writeable)
  # to keep track of the current value of the registers and read it before every write
  # So reads will use: 
  # reg = i2c.read_i2c_block_data(address, shadow_register[0], 32)
  # The function decode_reg(reg, reg_no, low_bit, high_bit) is used to pull a certain value from reg using the bits specified in the Si Labs spec 

  # NOTE about register writes:
  # i2c.write_i2c_block_data(address, value, byte_list) writes value to Reg 2 high and then the list to 2 low, 3 high, 3 low etc.
  # where value is an 8-bit byte (expressed as an integer) and byte_list is the list of 8-bit bytes
  # We will use shadow register described above for the write as follows:
  # i2c.write_i2c_block_data(address, shadow_register[0], shadow_register[1:])
  # So to do a write (this is done in write_register):
  # reg = i2c.read_i2c_block_data(address, shadow_register[0], 32)             # Read current register
  # shadow_register = reg[16:28]                                               # Copy current register values 2 - 7 into shadow register
  # update_shadow_register(shadow_register, reg_no, low_bit, high_bit, value)  # Update shadow_register reg_no[low_bit:high_bit] with value 
  # i2c.write_i2c_block_data(address, shadow_register[0], shadow_register[1:]) # Write register

# print("Initial Register Readings")
# reg = i2c.read_i2c_block_data(address, 0, 32)  # Nothing in Reg 2 high set yet
# print("Register 00h - Device ID Part Number: " + hex(decode_reg(reg, 0x00, 12, 14)))
# print("Register 00h - Device ID Manufacturer ID: " + hex(decode_reg(reg, 0x00, 0, 11)))

  # Follow AN230 Powerup Configuration Sequence (p. 12)
  # Set the XOSCEN bit (Reg 7 [15]) to power up the crystal
  shadow_register = [0] * 12    # Nothing in Reg 2 high set yet
  shadow_register = write_register(i2c, address, shadow_register, 7, 15, 15, 1)
  print("Powering up the crystal")
  #  Wait for crystal to power up (required for crystal oscillator operation), minimum 500 ms
  sleep(.5)
  print("Crystal now powered up")
  
  # Set DMUTE Reg 2 [14] to disable mute and ENABLE Reg 2 [0] to set the powerup state
  print("Powering up the device")
  shadow_register = write_register(i2c, address, shadow_register, 2, 14, 14, 1)
  shadow_register = write_register(i2c, address, shadow_register, 2, 0, 0, 1)
  #  Wait for device to powerup
  sleep(.1)
  print("Device now powered up")
   
  # VOLUME (Reg 5 [0-3]): 
  # 0x1 (1) is the lowest (good for earphones), 0xF (15) is the highest (good for speakers)
  shadow_register = write_register(i2c, address, shadow_register, 5, 0, 3, volume)
     
  # To tune the desired station write channel to CHAN (Reg 3 [0-9]) and set TUNE (Reg 3 [15])
  # Channel is defined as an integer representing how many steps it is above minimum 
  # FM regional frequency given regional spacing
  SPACE = .2       # US spacing between FM stations in MHz
  MIN_FREQ = 87.5  # US minimum FM frequency in MHz
  channel = round((freq - MIN_FREQ) / SPACE)
  shadow_register = write_register(i2c, address, shadow_register, 3, 0, 9, channel)
  shadow_register = write_register(i2c, address, shadow_register, 3, 15, 15, 1)


def initialize_si4703(bus):
  # Initialization based on Pi Flyer in thread at 
  #   https://www.raspberrypi.org/forums/viewtopic.php?f=41&t=28920&start=25&sid=eb677d15d2577540ab9e7f68ce76b35a
  # To add a 2nd i2c bus, see
  #   https://www.instructables.com/Raspberry-PI-Multiple-I2c-Devices/
  #
  # Wiring chart: 
  # Bus 1
  # SDA GPIO   (wired to Si4703 SDIO)  BCM GPIO 2 (physical pin 3)
  # SCL GPIO   (wired to Si4703 SCLK)  BCM GPIO 3 (physical pin 5)
  # Reset GPIO (wired to Si4703 ~RST)  BCM GPIO 4 (physical pin 7)
  # Bus 3
  # SDA GPIO   (wired to Si4703 SDIO)  BCM GPIO 23 (physical pin 16)
  # SCL GPIO   (wired to Si4703 SCLK)  BCM GPIO 24 (physical pin 18)
  # Reset GPIO (wired to Si4703 ~RST)  BCM GPIO 25 (physical pin 22)

  if (bus == 1):
    sda_gpio = 2
    scl_gpio = 3
    rst_gpio = 4
  if (bus == 3):
    sda_gpio = 23
    scl_gpio = 24
    rst_gpio = 25

  # Set the GPIO module to reference BCM pin numbers
  GPIO.setmode(GPIO.BCM)
  GPIO.setwarnings(False)
  
  # Setup Reset GPIO as output
  GPIO.setup(rst_gpio, GPIO.OUT)
  
  # Setup SDA GPIO as output
  # It needs to temporarily act as output to set the Si4703 to 2-wire (i2c) mode
  # Then we will set it back to i2c SDA
  GPIO.setup(sda_gpio, GPIO.OUT)
  
  # The Si4703 requires the following sequence to set it's busmode to 2-wire (i2c): 
  # Hold the SDIO pin low while the RST pin transitions from low to high
  GPIO.output(sda_gpio, GPIO.LOW) 
  sleep(.1) #allow pin to settle
  GPIO.output(rst_gpio, GPIO.LOW)
  sleep(.1)
  GPIO.output(rst_gpio, GPIO.HIGH)
  sleep(.1)
  
  # Setup SDA GPIO back to i2c SDA line
  # Make use of the subprocess module to execute the gpio program included
  # with WiringPi from the Raspbian commandline.
  # 'gpio' is the program to be executed, and the rest are options
  # '-g' is a flag that causes pin numbers to be interpreted as BCM_GPIO
  # 'mode' is the option used to select the mode of the pin
  # 'sda_gpio' is the pin to change the mode of
  # 'ALT0' is the alternate pin mode code for i2c 
  subprocess.check_output(['gpio', '-g', 'mode', str(sda_gpio), 'ALT0'])

def decode_reg(reg, reg_no, low_bit, high_bit):
  # Added by Dave Jaffe
  # get latest set of register values with: reg = i2c.read_i2c_block_data(address, value, 32)
  # Use low_bit, high_bit from Si4702-03-C19 spec
  # For example: CHANNEL (Reg 3 [0-9]) would be decoded from reg: decode_reg(reg, 3, 0, 9)
  # We will combine high and low bytes since Silicon Labs register spec refers to entire 16-bit word

  # reg byte layout: 0A high, 0A low ... 0F high, 0F low, 00 high, 00 low .. 09 high, 09 low
  # The mapping between a register number reg_no and the location of its high byte in reg is
  ind = 2 * ((16 + (reg_no - 0xA)) % 16)
  H = reg[ind]      # Register high byte
  L = reg[ind + 1]  # Register low byte
  W = 256*H + L     # Register 16-bit word
  # Use bit mask to pull desired bits, return them as an integer
  mask_len = high_bit - low_bit + 1
  mask = 2**mask_len - 1
  return (((mask << low_bit) & W) >> low_bit)
 
def update_shadow_register(shad_reg, reg_no, low_bit, high_bit, value):
  # Added by Dave Jaffe
  # Set shadow_register values
  # Use low_bit, high_bit from Si4702-03-C19 spec
  # For example, to set CHANNEL (Reg 3 [0-9]) update_shadow_register(shadow_register, 3, 0, 9, channel)
  # We will combine high and low bytes since Silicon Labs register spec refers to entire 16-bit word

  # shadow_register layout: Reg 2 high, 2 low, ... 7 high, 7 low (only Reg 2 - 7 are writeable)
  # The mapping between a register number reg_no and the location of its high byte in shadow_register is
  ind = (reg_no-2) * 2
  H = shad_reg[ind]      # Register high byte
  L = shad_reg[ind + 1]  # Register low byte
  W = 256*H + L     # Register 16-bit word
# print(format(W, '016b'))
  # First, set bits where new value will go to 0 with a negative mask
  mask_len = high_bit - low_bit + 1
  mask = 2**mask_len - 1
  mask = mask << low_bit
  W = W & ~mask
  # Next, move new value to proper bit and add it to W
  V = value << low_bit
  W = W  + V
  # Break up W into H and L and write to shadow_register
  H = W >> 8
  L = W & 2**8 - 1 
  shad_reg[ind] = H
  shad_reg[ind + 1] = L

def write_register(i2c, address, shad_reg, reg_no, low_bit, high_bit, value):
  reg = i2c.read_i2c_block_data(address, shad_reg[0], 32)
  shad_reg = reg[16:28]
  update_shadow_register(shad_reg, reg_no, low_bit, high_bit, value)
  i2c.write_i2c_block_data(address, shad_reg[0], shad_reg[1:])
  return (shad_reg)

if __name__ == "__main__":
  main()

si4703_rds_all.py

Python
si4703_rds_all.py: scan specified frequencies, playing station and printing out RadioText and RadioTextPlus
Syntax: python si4703_rds_all.py ic2_bus volume freqs_file
# Connects Silicon Labs Si4703 radio receiver (from Sparkfun kit WRL-12938) with Raspberry Pi 2 Model B v1.1
# si4703_rds_all.py - scan specified frequencies, playing station and printing out RadioText and RadioTextPlus
# Syntax: python si4703_rds_all.py i2c_bus volume freqs_file

# Silicon Labs Documentation:
# Operating instructions: https://www.silabs.com/documents/public/application-notes/AN230.pdf (AN230)
# Registers: https://www.silabs.com/documents/public/data-sheets/Si4702-03-C19.pdf (Si4702-03-C19)

# Register read/write information from KansasCoder (see https://www.raspberrypi.org/forums/viewtopic.php?t=28920)
# Initialization based on Pi Flyer in thread at
#   https://www.raspberrypi.org/forums/viewtopic.php?f=41&t=28920&start=25&sid=eb677d15d2577540ab9e7f68ce76b35a

import sys
from os import path
import RPi.GPIO as GPIO
import smbus
from time import time, sleep
from datetime import datetime
import subprocess


def main():
  
  # Check correct number of arguments, read them
  if (len(sys.argv) != 4):
    print("Syntax: python si4703_rds_all.py i2c_bus volume freqs_file")
    exit(-1)
  else:
    i2c_bus = int(sys.argv[1])
    volume = int(sys.argv[2])
    freqs_file = sys.argv[3]
    print("Scanning specified frequencies from file %s on radio on i2c bus %d with volume %d" % (freqs_file, i2c_bus, volume))
  
  # Check for frequencies file
  if not path.exists(freqs_file):
    print('Need file ' + freqs_file); exit()
  else:
    f_freqs = open(freqs_file, "r")

  initialize_si4703(i2c_bus)
  i2c = smbus.SMBus(i2c_bus)
  address = 0x10 #address of SI4703
  
  # Si4703 contains 32 8-bit integers, each corresponding to a byte of a 16-bit word:
  # reg = i2c.read_i2c_block_data(address, value, 32) returns them as:
  # 0A high, 0A low ... 0F high, 0F low, 00 high, 00 low .. 09 high, 09 low
  # We will combine high and low bytes since Silicon Labs register spec refers to entire 16-bit word
  # The mapping between a register number reg_no and the location of its high byte in reg is
  # ind = 2 * ((16 + (reg_no - 0xA)) % 16)

  # NOTE about register reads: 
  # i2c.read_i2c_block_data(address, value, 32) also WRITES value to Reg 2 high
  # as discovered by KansasCoder (see https://www.raspberrypi.org/forums/viewtopic.php?t=28920)
  # Thus we must always keep track of Reg 2 high, and then send it as i2c.read_i2c_block_data 2nd parameter
  # We will define 
  # shadow_register = reg[16:28]   # Reg 2 high, 2 low, ... 7 high, 7 low (only Reg 2 - 7 are writeable)
  # to keep track of the current value of the registers and read it before every write
  # So reads will use: 
  # reg = i2c.read_i2c_block_data(address, shadow_register[0], 32)
  # The function decode_reg(reg, reg_no, low_bit, high_bit) is used to pull a certain value from reg using the bits specified in the Si Labs spec 

  # NOTE about register writes:
  # i2c.write_i2c_block_data(address, value, byte_list) writes value to Reg 2 high and then the list to 2 low, 3 high, 3 low etc.
  # where value is an 8-bit byte (expressed as an integer) and byte_list is the list of 8-bit bytes
  # We will use shadow register described above for the write as follows:
  # i2c.write_i2c_block_data(address, shadow_register[0], shadow_register[1:])
  # So to do a write (this is done in function write_register):
  # reg = i2c.read_i2c_block_data(address, shadow_register[0], 32)             # Read current register
  # shadow_register = reg[16:28]                                               # Copy current register values 2 - 7 into shadow register
  # update_shadow_register(shadow_register, reg_no, low_bit, high_bit, value)  # Update shadow_register reg_no[low_bit:high_bit] with value 
  # i2c.write_i2c_block_data(address, shadow_register[0], shadow_register[1:]) # Write register

  # Follow AN230 Powerup Configuration Sequence (p. 12)
  # Set the XOSCEN bit (Reg 7 [15]) to power up the crystal
  shadow_register = [0] * 12    # Nothing in Reg 2 high set yet
  shadow_register = write_register(i2c, address, shadow_register, 7, 15, 15, 1)

  print("Powering up the crystal")
  #  Wait for crystal to power up (required for crystal oscillator operation), minimum 500 ms
  sleep(.5)
  print("Crystal now powered up")
  
  # Set DMUTE Reg 2 [14] to disable mute and ENABLE Reg 2 [0] to set the powerup state
  print("Powering up the device")
  shadow_register = write_register(i2c, address, shadow_register, 2, 14, 14, 1)
  shadow_register = write_register(i2c, address, shadow_register, 2, 0, 0, 1)
  #  Wait for device to powerup
  sleep(.1)
  print("Device now powered up")

  # VOLUME (Reg 5 [0-3]):
  # 0x1 (1) is the lowest (good for earphones), 0xF (15) is the highest (good for speakers)
  shadow_register = write_register(i2c, address, shadow_register, 5, 0, 3, volume)

  # Enable RDS (Reg 4 [12])
  # See https://www.silabs.com/documents/public/application-notes/AN243.pdf for RDS reception details
  # See http://www.interactive-radio-system.com/docs/EN50067_RDS_Standard.pdf for Radiotext (RT) details
  shadow_register = write_register(i2c, address, shadow_register, 4, 12, 12, 1)
  print("RDS Enabled")
 
  # Parse frequencies file
  # Format:
  # freqs = [93.7, 97.1, 98.1, 98.5, 98.9, 100.1, 100.7]
  # freqs_with_RT_plus = [96.7, 98.1, 100.7]             - optional
  freqs = []
  freqs_with_RT_plus = []
  freqs_line = f_freqs.readline()
  if (len(freqs_line) == 0):
    print('No frequencies specified')
    f_freqs.close()
    exit(-1)
  freqs = eval(freqs_line[freqs_line.index('['):])
  freqs_with_RT_plus_line = f_freqs.readline()
  if (len(freqs_with_RT_plus_line) != 0):
    freqs_with_RT_plus = eval(freqs_with_RT_plus_line[freqs_with_RT_plus_line.index('['):])
  print('Freqs to scan = ' + str(freqs))
  print('Freqs with RT_plus = ' + str(freqs_with_RT_plus))
  
  # Start scanning frequencies for RDS and RDS_plus info
  time_out_sec = 15
  while (True):
    for freq in freqs:
#     print('\n' + ('%5.1f' % (freq)))
      # To tune the desired station write channel to CHAN (Reg 3 [0-9]) and set TUNE (Reg 3 [15])
      # Channel is defined as an integer representing how many steps it is above minimum
      # FM regional frequency given regional spacing
      SPACE = .2       # US spacing between FM stations in MHz
      MIN_FREQ = 87.5  # US minimum FM frequency in MHz
      channel = round((freq - MIN_FREQ) / SPACE)
      shadow_register = write_register(i2c, address, shadow_register, 3, 0, 9, channel)
      shadow_register = write_register(i2c, address, shadow_register, 3, 15, 15, 1)
      sleep(1)
      # Unset TUNE (Reg 3 [15]) to be able to retune
      shadow_register = write_register(i2c, address, shadow_register, 3, 15, 15, 0)
      sleep(1)
      
      # Obtain Radio Text (RT)
      # RT (and other info) is sent in 16-bit words in RDSA, RDSB, RDSC, RDSD in reg C, D, E and F
      # RDSA and RDSB indicate the Program Identifier (PI) or station ID and Group Type Code 
      # (what type of data is being sent - RT, traffic, etc.)
      # The RT is read in 4-character segments from unicode characters from RDSC and RDSD
      # The RT segment index (RDSB (Reg D) [0-3]) segment 0 indicates the start of the RT
      # The end of the RT transmission is shown by either a carriage return (CR - decimal 13) as one of the 4 chars,
      # a blank line (4 decimal 32 chars), or reaching the 15th segment (max 64 chars in RT)
      s_prev = ''             # The previously decoded string
      RT = ''                 # The RT string being built
      seen_RT_seg0 = False    # Whether the RT segment 0 has been seen yet
      found_RT = False        # Whether we've found the RT string yet
      no_RT = False           # If no RT found before timeout
      RT_start_time = time()
      artist = ''
      title = ''
      prev_seg_ind = -1
      while(not found_RT):
        reg = i2c.read_i2c_block_data(address, shadow_register[0], 32)
        if (decode_reg(reg, 0x0D, 12, 15) == 0x2):    # Wait for RDSB (reg D) [12-15] to show Group Type 0x2 => Radio Text (Group 2A) 
          seg_ind = decode_reg(reg, 0x0D, 0, 3)           # RT segment index
#         print(seg_ind)
          if ((seg_ind != 0) and (not seen_RT_seg0)):
            if (time() - RT_start_time) > time_out_sec:   # Bail if no RT within time_out_sec seconds
#             print('No RT segment 0 found within ' + str(time_out_sec) + ' sec')
              no_RT = True
              break
            else:
              continue                                    # Wait for RT segment 0
          else:
            seen_RT_seg0 = True
            if (seg_ind == prev_seg_ind + 1):             # Next RT segment
              CH = decode_reg(reg, 0x0E, 8, 15)           # Pull 4 unicode chars as integers from RDSC and RDSD
              CL = decode_reg(reg, 0x0E, 0, 7)
              DH = decode_reg(reg, 0x0F, 8, 15)
              DL = decode_reg(reg, 0x0F, 0, 7)
              if (CH in range(32,127)): chrCH = chr(CH)   # Unicode character corresponding to integer
              else: chrCH = ' '                           # Replace non-printing chars with blanks
              if (CL in range(32,127)): chrCL = chr(CL)
              else: chrCL = ' '
              if (DH in range(32,127)): chrDH = chr(DH)
              else: chrDH = ' '
              if (DL in range(32,127)): chrDL = chr(DL)
              else: chrDL = ' '
              s = chrCH + chrCL + chrDH + chrDL
              if (s != s_prev):                           # If this is a new piece of the string add it to the RT
                RT = RT + s
              s_prev = s
#             print(CH, CL, DH, DL)
#             print(str(seg_ind) + s)
              if (CH == 13 or CL == 13 or DH == 13 or DL == 13 or seg_ind == 15 or (CH == 32 and CL == 32 and DH == 32 and DL == 32)):
                found_RT = True 
#               print(RT)
                prev_seg_ind = -1
              else:
                prev_seg_ind = seg_ind
            if (time() - RT_start_time) > time_out_sec:   # Bail if no RT within time_out_sec seconds
#             print('RT not complete within ' + str(time_out_sec) + ' sec')
              no_RT = True
              break
        elif (time() - RT_start_time) > time_out_sec:   # Bail if no Group 2A within time_out_sec seconds
#         print('No Group 2A found within ' + str(time_out_sec) + ' sec')
          no_RT = True
          break
        
      if (no_RT == False):
#       print("Time to find RadioText: %5.1f sec" % (time() - RT_start_time))
        # Now that we've found RT find RT+ data for those stations known to have it, 
        # which shows where in the RT string the song title and artist are
        # For RT_plus format see https://tech.ebu.ch/docs/techreview/trev_307-radiotext.pdf
        if (freq in freqs_with_RT_plus): 
          found_RT_plus = False       # Whether we've found the RT+ info
          RTplus_start_time = time()
          while (not found_RT_plus):
            reg = i2c.read_i2c_block_data(address, shadow_register[0], 32)
            # Group 3 indicates that this transmission includes the Applications Identification (AID) for an Open Data Applications (ODA)
            #   You can verify with: print(decode_reg(reg, 0x0D, 12, 15))
            # RDSD (reg F) = 0x4bd7 is the AID for RT+ and RDSB bits 4 - 1 = 0xc indicates ODA type RT+
            if ((hex(decode_reg(reg, 0x0F, 8, 15)) == '0x4b') and (hex(decode_reg(reg, 0x0F, 0, 7)) == '0xd7') and (hex(decode_reg(reg, 0x0D, 1, 4)) == '0xc')):
#             print('0x4bd7 seen')
              while(not found_RT_plus):
                reg = i2c.read_i2c_block_data(address, shadow_register[0], 32)
                # If the Group Type is 0xc then we have RT+ data
                if (hex(decode_reg(reg, 0x0D, 12, 15)) == '0xc'):
                  item_toggle_bit =  decode_reg(reg, 0x0D, 4, 4)       # Item toggle bit - toggled when new item starts - not used here
                  item_running_bit = decode_reg(reg, 0x0D, 3, 3)       # Item running bit - toggled when item is running - not used here
                  item1_type  = decode_reg(reg, 0x0E, 13, 15)          # Item 1 type:  4 = artist   1 = song title
                  item1_start = decode_reg(reg, 0x0E, 7, 12)
                  item1_end   = decode_reg(reg, 0x0E, 1, 6)
                  item2_type  = decode_reg(reg, 0x0F, 11, 15)          # Item 2 type:  4 = artist   1 = song title
                  item2_start = decode_reg(reg, 0x0F, 5, 10)
                  item2_end   = decode_reg(reg, 0x0F, 0, 4)
                  if (item1_type == 4):
                    artist = RT[item1_start : 1 + item1_start + item1_end]
                  elif (item2_type == 4):
                    artist = RT[item2_start : 1 + item2_start + item2_end]
                  if (item1_type == 1):
                    title = RT[item1_start : 1 + item1_start + item1_end]
                  elif (item2_type == 1):
                    title = RT[item2_start : 1 + item2_start + item2_end]
                  elif (item1_type != 1 and item1_type != 4 and item2_type != 1 and item1_type != 4):
#                   print('RT+ does not contain title or artist')
                    pass
                  found_RT_plus = True
            elif (time() - RTplus_start_time) > time_out_sec:   # Bail if no RT+ within time_out_sec seconds
#             print('No RT+ found within ' + str(time_out_sec) + ' sec')
              break
        else:    # Parse artist and title from RT if they're not in RT+
          # Search RT for "by" or "/", split into title and artist
          if (RT.find('by') > -1):
            title = RT[:RT.find('by')-1]
            artist = RT[RT.find('by')+3:]
            # Strip out "now" or "on" from artist
            if (artist.find(' now ') > -1):
              artist = artist[:artist.find(' now ')]
            if (artist.find(' on ') > -1):
              artist = artist[:artist.find(' on ')]
            # Strip out "Now Playing" from title
            if (title.find('Now Playing ') > -1):
              title = title[title.find('Now Playing ')+12:]
          elif (RT.find('/') > -1):
            artist = RT[:RT.find('/')]
            title = RT[RT.find('/')+2:]
            # Strip out "now" or "on" from artist
            if (artist.find(' now ') > -1):
              artist = artist[:artist.find(' now ')]
            if (artist.find(' on ') > -1):
              artist = artist[:artist.find(' on ')]
            # Strip out "Now Playing" from title
            if (title.find('Now Playing ') > -1):
              title = title[title.find('Now Playing ')+12:]
  
      # Output info
      if (RT == ''):
        output = ('%5.1f' % (freq))
      elif ((artist == '') and (title == '')):
        output = ('%5.1f' % (freq)) + '  ' + RT
      elif ((artist != '') and (title == '')):
        output = ('%5.1f' % (freq)) + '  ' + artist
      elif ((artist != '') and (title != '')):
        output = ('%5.1f' % (freq)) + '  ' + artist + '   ' + title
      print(datetime.now().strftime("%A %D %H:%M:%S") + ':  ' + output)
  

def initialize_si4703(bus):
  # Initialization based on Pi Flyer in thread at 
  #   https://www.raspberrypi.org/forums/viewtopic.php?f=41&t=28920&start=25&sid=eb677d15d2577540ab9e7f68ce76b35a
  # To add a 2nd i2c bus, see
  #   https://www.instructables.com/Raspberry-PI-Multiple-I2c-Devices/
  #
  # Wiring chart: 
  # Bus 1
  # SDA GPIO   (wired to Si4703 SDIO)  BCM GPIO 2 (physical pin 3)
  # SCL GPIO   (wired to Si4703 SCLK)  BCM GPIO 3 (physical pin 5)
  # Reset GPIO (wired to Si4703 ~RST)  BCM GPIO 4 (physical pin 7)
  # Bus 3
  # SDA GPIO   (wired to Si4703 SDIO)  BCM GPIO 23 (physical pin 16)
  # SCL GPIO   (wired to Si4703 SCLK)  BCM GPIO 24 (physical pin 18)
  # Reset GPIO (wired to Si4703 ~RST)  BCM GPIO 25 (physical pin 22)

  if (bus == 1):
    sda_gpio = 2
    scl_gpio = 3
    rst_gpio = 4
  if (bus == 3):
    sda_gpio = 23
    scl_gpio = 24
    rst_gpio = 25

  # Set the GPIO module to reference BCM pin numbers
  GPIO.setmode(GPIO.BCM)
  GPIO.setwarnings(False)
  
  # Setup Reset GPIO as output
  GPIO.setup(rst_gpio, GPIO.OUT)
  
  # Setup SDA GPIO as output
  # It needs to temporarily act as output to set the Si4703 to 2-wire (i2c) mode
  # Then we will set it back to i2c SDA
  GPIO.setup(sda_gpio, GPIO.OUT)
  
  # The Si4703 requires the following sequence to set it's busmode to 2-wire (i2c): 
  # Hold the SDIO pin low while the RST pin transitions from low to high
  GPIO.output(sda_gpio, GPIO.LOW) 
  sleep(.1) #allow pin to settle
  GPIO.output(rst_gpio, GPIO.LOW)
  sleep(.1)
  GPIO.output(rst_gpio, GPIO.HIGH)
  sleep(.1)
  
  # Setup SDA GPIO back to i2c SDA line
  # Make use of the subprocess module to execute the gpio program included
  # with WiringPi from the Raspbian commandline.
  # 'gpio' is the program to be executed, and the rest are options
  # '-g' is a flag that causes pin numbers to be interpreted as BCM_GPIO
  # 'mode' is the option used to select the mode of the pin
  # 'sda_gpio' is the pin to change the mode of
  # 'ALT0' is the alternate pin mode code for i2c 
  subprocess.check_output(['gpio', '-g', 'mode', str(sda_gpio), 'ALT0'])

def decode_reg(reg, reg_no, low_bit, high_bit):
  # Added by Dave Jaffe
  # get latest set of register values with: reg = i2c.read_i2c_block_data(address, value, 32)
  # Use low_bit, high_bit from Si4702-03-C19 spec
  # For example: CHANNEL (Reg 3 [0-9]) would be decoded from reg: decode_reg(reg, 3, 0, 9)
  # We will combine high and low bytes since Silicon Labs register spec refers to entire 16-bit word

  # reg byte layout: 0A high, 0A low ... 0F high, 0F low, 00 high, 00 low .. 09 high, 09 low
  # The mapping between a register number reg_no and the location of its high byte in reg is
  ind = 2 * ((16 + (reg_no - 0xA)) % 16)
  H = reg[ind]      # Register high byte
  L = reg[ind + 1]  # Register low byte
  W = 256*H + L     # Register 16-bit word
  # Use bit mask to pull desired bits, return them as an integer
  mask_len = high_bit - low_bit + 1
  mask = 2**mask_len - 1
  return (((mask << low_bit) & W) >> low_bit)
 
def update_shadow_register(shad_reg, reg_no, low_bit, high_bit, value):
  # Added by Dave Jaffe
  # Set shadow_register values
  # Use low_bit, high_bit from Si4702-03-C19 spec
  # For example, to set CHANNEL (Reg 3 [0-9]) update_shadow_register(shadow_register, 3, 0, 9, channel)
  # We will combine high and low bytes since Silicon Labs register spec refers to entire 16-bit word

  # shadow_register layout: Reg 2 high, 2 low, ... 7 high, 7 low (only Reg 2 - 7 are writeable)
  # The mapping between a register number reg_no and the location of its high byte in shadow_register is
  ind = (reg_no-2) * 2
  H = shad_reg[ind]      # Register high byte
  L = shad_reg[ind + 1]  # Register low byte
  W = 256*H + L     # Register 16-bit word
# print(format(W, '016b'))
  # First, set bits where new value will go to 0 with a negative mask
  mask_len = high_bit - low_bit + 1
  mask = 2**mask_len - 1
  mask = mask << low_bit
  W = W & ~mask
  # Next, move new value to proper bit and add it to W
  V = value << low_bit
  W = W  + V
  # Break up W into H and L and write to shadow_register
  H = W >> 8
  L = W & 2**8 - 1 
  shad_reg[ind] = H
  shad_reg[ind + 1] = L

def write_register(i2c, address, shad_reg, reg_no, low_bit, high_bit, value):
  reg = i2c.read_i2c_block_data(address, shad_reg[0], 32)
  shad_reg = reg[16:28] 
  update_shadow_register(shad_reg, reg_no, low_bit, high_bit, value)
  i2c.write_i2c_block_data(address, shad_reg[0], shad_reg[1:])
  return (shad_reg)

if __name__ == "__main__":
  main()

si4703_player.py

Python
si4703_player.py: Reads specified frequency and volume from file freq_vol.txt, plays station, checks file in loop
Syntax: python si4703_player.py i2c_bus
freq_vol.txt example: 98.1 15
# Connects Silicon Labs Si4703 radio receiver (from Sparkfun kit WRL-12938) with Raspberry Pi 2 Model B v1.1
# si4703_player.py: reads specified frequency and volume from file, plays station, checks file in loop
# Syntax: python si4703_player.py i2c_bus 

# Silicon Labs Documentation:
# Operating instructions: https://www.silabs.com/documents/public/application-notes/AN230.pdf (AN230)
# Registers: https://www.silabs.com/documents/public/data-sheets/Si4702-03-C19.pdf (Si4702-03-C19)

import sys
import RPi.GPIO as GPIO
import smbus
from time import time, sleep
import subprocess
from os import path

def main():
  # Check correct number of arguments, read them
  if (len(sys.argv) != 2): 
    print("Syntax: python si4703_player.py i2c_bus")
    exit(-1)
  else:
    i2c_bus = int(sys.argv[1])
    print("Playing radio on i2c bus %d" % (i2c_bus))

  initialize_si4703(i2c_bus)
  i2c = smbus.SMBus(i2c_bus)
  address = 0x10 #address of SI4703
  
  # Si4703 contains 32 8-bit integers, each corresponding to a byte of a 16-bit word:
  # reg = i2c.read_i2c_block_data(address, value, 32) returns them as:
  # 0A high, 0A low ... 0F high, 0F low, 00 high, 00 low .. 09 high, 09 low
  # We will combine high and low bytes since Silicon Labs register spec refers to entire 16-bit word
  # The mapping between a register number reg_no and the location of its high byte in reg is
  # ind = 2 * ((16 + (reg_no - 0xA)) % 16)

  # NOTE about register reads: 
  # i2c.read_i2c_block_data(address, value, 32) also WRITES value to Reg 2 high
  # as discovered by KansasCoder (see https://www.raspberrypi.org/forums/viewtopic.php?t=28920)
  # Thus we must always keep track of Reg 2 high, and then send it as i2c.read_i2c_block_data 2nd parameter
  # We will define 
  # shadow_register = reg[16:28]   # Reg 2 high, 2 low, ... 7 high, 7 low (only Reg 2 - 7 are writeable)
  # to keep track of the current value of the registers and read it before every write
  # So reads will use: 
  # reg = i2c.read_i2c_block_data(address, shadow_register[0], 32)
  # The function decode_reg(reg, reg_no, low_bit, high_bit) is used to pull a certain value from reg using the bits specified in the Si Labs spec 

  # NOTE about register writes:
  # i2c.write_i2c_block_data(address, value, byte_list) writes value to Reg 2 high and then the list to 2 low, 3 high, 3 low etc.
  # where value is an 8-bit byte (expressed as an integer) and byte_list is the list of 8-bit bytes
  # We will use shadow register described above for the write as follows:
  # i2c.write_i2c_block_data(address, shadow_register[0], shadow_register[1:])
  # So to do a write (this is done in write_register):
  # reg = i2c.read_i2c_block_data(address, shadow_register[0], 32)             # Read current register
  # shadow_register = reg[16:28]                                               # Copy current register values 2 - 7 into shadow register
  # update_shadow_register(shadow_register, reg_no, low_bit, high_bit, value)  # Update shadow_register reg_no[low_bit:high_bit] with value 
  # i2c.write_i2c_block_data(address, shadow_register[0], shadow_register[1:]) # Write register

  # Follow AN230 Powerup Configuration Sequence (p. 12)
  # Set the XOSCEN bit (Reg 7 [15]) to power up the crystal
  shadow_register = [0] * 12    # Nothing in Reg 2 high set yet
  shadow_register = write_register(i2c, address, shadow_register, 7, 15, 15, 1)
  print("Powering up the crystal")
  #  Wait for crystal to power up (required for crystal oscillator operation), minimum 500 ms
  sleep(.5)
  print("Crystal now powered up")
  
  # Set DMUTE Reg 2 [14] to disable mute and ENABLE Reg 2 [0] to set the powerup state
  print("Powering up the device")
  shadow_register = write_register(i2c, address, shadow_register, 2, 14, 14, 1)
  shadow_register = write_register(i2c, address, shadow_register, 2, 0, 0, 1)
  #  Wait for device to powerup
  sleep(.1)
  print("Device now powered up")
   
  # Read frequency and volume from freq_vol.txt in continuous loop
  if not path.exists("/home/pi/si4703/freq_vol.txt"):
    print("Need file freq_vol.txt"); exit()

  prev_freq =  0.0
  prev_volume = 0
  while True:
    f_freq_vol = open("/home/pi/si4703/freq_vol.txt", "r")
    freq_vol = f_freq_vol.readline()
    f_freq_vol.close()
    if (len(freq_vol) == 0):
      sleep(1)
      continue
    freq_vol = freq_vol.lstrip()  # Remove leading blank if any
    space_ind = freq_vol.find(' ')
    freq = float(freq_vol[:space_ind])
    if (freq == 0.0): break
    volume = int(freq_vol[space_ind+1:])
#   print('Playing station %5.1f at volume %d' % (freq, volume))
  
    if ((freq != prev_freq) or (volume != prev_volume)):
      # VOLUME (Reg 5 [0-3]): 
      # 0x1 (1) is the lowest (good for earphones), 0xF (15) is the highest (good for speakers)
      shadow_register = write_register(i2c, address, shadow_register, 5, 0, 3, volume)
     
      # To tune the desired station write channel to CHAN (Reg 3 [0-9]) and set TUNE (Reg 3 [15])
      # Channel is defined as an integer representing how many steps it is above minimum 
      # FM regional frequency given regional spacing
      SPACE = .2       # US spacing between FM stations in MHz
      MIN_FREQ = 87.5  # US minimum FM frequency in MHz
      channel = round((freq - MIN_FREQ) / SPACE)
      shadow_register = write_register(i2c, address, shadow_register, 3, 0, 9, channel)
      shadow_register = write_register(i2c, address, shadow_register, 3, 15, 15, 1)

      prev_freq = freq
      prev_volume = volume
      # Unset TUNE (Reg 3 [15]) to be able to retune
      shadow_register = write_register(i2c, address, shadow_register, 3, 15, 15, 0)
      sleep(1) # Then return to top of while loop and check freq_vol.txt for a new freq/vol

# End main()

def initialize_si4703(bus):
  # Initialization based on Pi Flyer in thread at 
  #   https://www.raspberrypi.org/forums/viewtopic.php?f=41&t=28920&start=25&sid=eb677d15d2577540ab9e7f68ce76b35a
  # To add a 2nd i2c bus, see
  #   https://www.instructables.com/Raspberry-PI-Multiple-I2c-Devices/
  #
  # Wiring chart: 
  # Bus 1
  # SDA GPIO   (wired to Si4703 SDIO)  BCM GPIO 2 (physical pin 3)
  # SCL GPIO   (wired to Si4703 SCLK)  BCM GPIO 3 (physical pin 5)
  # Reset GPIO (wired to Si4703 ~RST)  BCM GPIO 4 (physical pin 7)
  # Bus 3
  # SDA GPIO   (wired to Si4703 SDIO)  BCM GPIO 23 (physical pin 16)
  # SCL GPIO   (wired to Si4703 SCLK)  BCM GPIO 24 (physical pin 18)
  # Reset GPIO (wired to Si4703 ~RST)  BCM GPIO 25 (physical pin 22)

  if (bus == 1):
    sda_gpio = 2
    scl_gpio = 3
    rst_gpio = 4
  if (bus == 3):
    sda_gpio = 23
    scl_gpio = 24
    rst_gpio = 25

  # Set the GPIO module to reference BCM pin numbers
  GPIO.setmode(GPIO.BCM)
  GPIO.setwarnings(False)
  
  # Setup Reset GPIO as output
  GPIO.setup(rst_gpio, GPIO.OUT)
  
  # Setup SDA GPIO as output
  # It needs to temporarily act as output to set the Si4703 to 2-wire (i2c) mode
  # Then we will set it back to i2c SDA
  GPIO.setup(sda_gpio, GPIO.OUT)
  
  # The Si4703 requires the following sequence to set it's busmode to 2-wire (i2c): 
  # Hold the SDIO pin low while the RST pin transitions from low to high
  GPIO.output(sda_gpio, GPIO.LOW) 
  sleep(.1) #allow pin to settle
  GPIO.output(rst_gpio, GPIO.LOW)
  sleep(.1)
  GPIO.output(rst_gpio, GPIO.HIGH)
  sleep(.1)
  
  # Setup SDA GPIO back to i2c SDA line
  # Make use of the subprocess module to execute the gpio program included
  # with WiringPi from the Raspbian commandline.
  # 'gpio' is the program to be executed, and the rest are options
  # '-g' is a flag that causes pin numbers to be interpreted as BCM_GPIO
  # 'mode' is the option used to select the mode of the pin
  # 'sda_gpio' is the pin to change the mode of
  # 'ALT0' is the alternate pin mode code for i2c 
  subprocess.check_output(['gpio', '-g', 'mode', str(sda_gpio), 'ALT0'])

def decode_reg(reg, reg_no, low_bit, high_bit):
  # Added by Dave Jaffe
  # get latest set of register values with: reg = i2c.read_i2c_block_data(address, value, 32)
  # Use low_bit, high_bit from Si4702-03-C19 spec
  # For example: CHANNEL (Reg 3 [0-9]) would be decoded from reg: decode_reg(reg, 3, 0, 9)
  # We will combine high and low bytes since Silicon Labs register spec refers to entire 16-bit word

  # reg byte layout: 0A high, 0A low ... 0F high, 0F low, 00 high, 00 low .. 09 high, 09 low
  # The mapping between a register number reg_no and the location of its high byte in reg is
  ind = 2 * ((16 + (reg_no - 0xA)) % 16)
  H = reg[ind]      # Register high byte
  L = reg[ind + 1]  # Register low byte
  W = 256*H + L     # Register 16-bit word
  # Use bit mask to pull desired bits, return them as an integer
  mask_len = high_bit - low_bit + 1
  mask = 2**mask_len - 1
  return (((mask << low_bit) & W) >> low_bit)
 
def update_shadow_register(shad_reg, reg_no, low_bit, high_bit, value):
  # Added by Dave Jaffe
  # Set shadow_register values
  # Use low_bit, high_bit from Si4702-03-C19 spec
  # For example, to set CHANNEL (Reg 3 [0-9]) update_shadow_register(shadow_register, 3, 0, 9, channel)
  # We will combine high and low bytes since Silicon Labs register spec refers to entire 16-bit word

  # shadow_register layout: Reg 2 high, 2 low, ... 7 high, 7 low (only Reg 2 - 7 are writeable)
  # The mapping between a register number reg_no and the location of its high byte in shadow_register is
  ind = (reg_no-2) * 2
  H = shad_reg[ind]      # Register high byte
  L = shad_reg[ind + 1]  # Register low byte
  W = 256*H + L     # Register 16-bit word
# print(format(W, '016b'))
  # First, set bits where new value will go to 0 with a negative mask
  mask_len = high_bit - low_bit + 1
  mask = 2**mask_len - 1
  mask = mask << low_bit
  W = W & ~mask
  # Next, move new value to proper bit and add it to W
  V = value << low_bit
  W = W  + V
  # Break up W into H and L and write to shadow_register
  H = W >> 8
  L = W & 2**8 - 1 
  shad_reg[ind] = H
  shad_reg[ind + 1] = L

def write_register(i2c, address, shad_reg, reg_no, low_bit, high_bit, value):
  reg = i2c.read_i2c_block_data(address, shad_reg[0], 32)
  shad_reg = reg[16:28]
  update_shadow_register(shad_reg, reg_no, low_bit, high_bit, value)
  i2c.write_i2c_block_data(address, shad_reg[0], shad_reg[1:])
  return (shad_reg)

if __name__ == "__main__":
  main()

ScanRDS

Java
Android program to display RDS info from specified stations acquired by Raspberry Pi
Displays 5 most recent stations showing RDS info as well as 4 presets
Click on any button to play station
// ScanRDS
// Communicates with Raspberry Pi - based dual radio to display RDS info
// Displays 5 most recent stations showing RDS info as well as 4 presets
// Click on any button to play station

package com.example.djradio1;

import android.content.Intent;
import android.graphics.Color;
import android.graphics.Typeface;
import android.media.AudioManager;
import android.media.ToneGenerator;
import android.os.Bundle;
import android.os.AsyncTask;
import android.os.Build;

import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;

import android.view.View;
import android.view.Menu;
import android.view.MenuItem;

import android.util.Log;
import android.util.TypedValue;

import android.widget.Button;
import android.widget.Toast;

import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.StringRequest;
import com.android.volley.toolbox.Volley;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

import static java.lang.Thread.sleep;

public class MainActivity extends AppCompatActivity {

  static int n_rds_buttons = 5;
  static int n_preset_buttons = 4;
  private static final String TAG = MainActivity.class.getName();
  private RequestQueue queue;
  private StringRequest stringRequest;
  private String last_rds_info="", previous_rds_info="";
  private String[] rds_info = new String[n_rds_buttons];
  private String ip_address, url;
  private static final int[] rds_button_ids = {R.id.rds1Button, R.id.rds2Button,
      R.id.rds3Button, R.id.rds4Button, R.id.rds5Button};
  private static final int[] preset_button_ids = {R.id.preset1Button, R.id.preset2Button,
      R.id.preset3Button, R.id.preset4Button};
  private static final int[] preset_text_ids = {R.string.preset1Text, R.string.preset2Text,
      R.string.preset3Text, R.string.preset4Text};
  static String[] preset_freq = new String[n_preset_buttons];
  private Button rds_button, rds_button_current=null, preset_button, preset_button_current=null;
  private int index = -1;
  private Integer currentStation = null;
  private int primaryColor, primaryColorVariant;

  String ip_address_default = "192.168.2.101";

  final static int ACTIVITY_SETTINGS = 0;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Toolbar toolbar = findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    BackgroundGetRDS getlatestrds = new BackgroundGetRDS();

    //Find IP address of Raspberry Pi
    //region
    if (Build.FINGERPRINT.contains("generic"))
      ip_address = ip_address_default;   // Emulator
    else {   // Real device
      // Find IP address assigned to Raspberry Pi from Pixel Hotspot
      // ip neigh returns all IPs in neighborhood
      // One we want is 192.168.x.187 dev wlan1
      try {
        Process process = Runtime.getRuntime().exec("ip neigh");
        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        int read;
        char[] buffer = new char[4096];
        StringBuffer output = new StringBuffer();
        while ((read = reader.read(buffer)) > 0) {
          output.append(buffer, 0, read);
        }
        reader.close();
        process.waitFor();
        String out = output.toString();
        int end_ind = out.indexOf(" dev wlan1");
        if (end_ind != -1) {
          out = out.substring(0, end_ind);
          int start_ind = out.lastIndexOf("192");
          if (start_ind != -1)
          ip_address = out.substring(start_ind);
          else{
            Toast.makeText(getApplicationContext(), "Can't determine IP address, check Wifi enabled", Toast.LENGTH_LONG).show();
            ip_address = ip_address_default;
          }
        }
        else{
          Toast.makeText(getApplicationContext(), "Can't determine IP address, check wifi enabled", Toast.LENGTH_LONG).show();
          ip_address = ip_address_default;
        }
      } catch (IOException e) {
        throw new RuntimeException(e);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
    }
    Log.i(TAG, "ip address of Raspberry Pi = " + ip_address);
    //endregion Find IP address of Raspberry Pi

    // Set colors from theme
    final TypedValue value = new TypedValue();
    getTheme().resolveAttribute(R.attr.colorPrimary, value, true);
    primaryColor = value.data;
    getTheme().resolveAttribute(R.attr.colorPrimaryVariant, value, true);
    primaryColorVariant = value.data;

    // Set up presets
    //region
    for (int i=0; i < n_preset_buttons; i++) {
      preset_button = (Button) findViewById(preset_button_ids[i]);
      preset_freq[i] = getResources().getString(preset_text_ids[i]);
      Log.i(TAG, "preset_freq[" + i + "] = " + preset_freq[i]);
      preset_button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
          int button_id = view.getId();
          int button_clicked_index = 0;
          while (button_id != preset_button_ids[button_clicked_index++]) ;
          button_clicked_index--;
          Log.i(TAG, "button_id = " + button_id + " button_clicked_index = " + button_clicked_index);
          url = "http://" + ip_address + ":5000/playradio";
          String freq_vol = preset_freq[button_clicked_index] + " 15";
          Log.i(TAG, "In preset onClick: url = " + url + "freq_vol = " + freq_vol);
          sendPost(url, freq_vol);
          // Set color of new selected button to primaryColorVariant, set previous selected button back to primaryColor
          if (currentStation != null) {
            if (currentStation < n_rds_buttons) {
              rds_button_current = (Button) findViewById(rds_button_ids[currentStation]);
              Log.i(TAG, "current_station = " + currentStation + " rds_button_current_id = " + rds_button_ids[currentStation]);
              rds_button_current.setBackgroundColor(primaryColor);
            } else {
              preset_button_current = (Button) findViewById(preset_button_ids[currentStation - n_rds_buttons]);
              Log.i(TAG, "current_station = " + currentStation + " preset_button_current_id = " + preset_button_ids[currentStation - n_rds_buttons]);
              preset_button_current.setBackgroundColor(primaryColor);
            }
          }
          preset_button = (Button) findViewById(button_id);
          preset_button.setBackgroundColor(primaryColorVariant);
          currentStation = button_clicked_index + n_rds_buttons;
        }
      });
    }
    //endregion Set up presets

    // Set up Exit button
    Button exitButton = findViewById(R.id.exitButton);
    exitButton.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        getlatestrds.cancel(true);
        Log.i(TAG, "cancel getlatestrds sent");
      }
    });

    // Start queue of network requests
    queue = Volley.newRequestQueue(this);

    // Send GET Request to start RDS scan program
    url = "http://" + ip_address + ":5000/startrds";
    sendGet(url);
    try {
      sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }

    // Launch process to read RDS data from RaspberryPi
    getlatestrds.execute();

  } // End OnCreate

  class BackgroundGetRDS extends AsyncTask<Void, Integer, String> {
    @Override
    protected String doInBackground(Void... voids) {
      while(!isCancelled()){
        url = "http://" + ip_address + ":5000/getrds";
        sendGet(url);
        try {
          sleep(5000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        publishProgress();
      }
      return "Executed";
    }

    @Override
    protected void onProgressUpdate(Integer... param){
      // Called with publishProgress() from doInBackground
      Log.i(TAG, "In onProgressUpdate: last_rds_info = " + last_rds_info + " index = " + index);
      if (!last_rds_info.equals(previous_rds_info)) {
        index = ++index % n_rds_buttons;
        previous_rds_info = last_rds_info;
        rds_info[index] = last_rds_info;
        rds_button = (Button) findViewById(rds_button_ids[index]);
        rds_button.setText(last_rds_info);
        if (last_rds_info.contains("*")) {
          rds_button.setTextColor(Color.YELLOW);
          ToneGenerator beep = new ToneGenerator(AudioManager.STREAM_ALARM, 100);
          beep.startTone(ToneGenerator.TONE_PROP_BEEP);
        }
        else rds_button.setTextColor(Color.WHITE);
        rds_button.setBackgroundColor(primaryColor); // In case it is currentStation
        rds_button.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
            int button_id = view.getId();
            int button_clicked_index = 0;
            while (button_id != rds_button_ids[button_clicked_index++]); button_clicked_index--;
            Log.i(TAG, "button_id = " +  button_id + " button_clicked_index = " + button_clicked_index);
            String freq_vol = rds_info[button_clicked_index].substring(0, 5) + " 15";
            Log.i(TAG, "In OnProgressUpdate: freq_vol = " + freq_vol);
            url = "http://" + ip_address + ":5000/playradio";
            sendPost(url, freq_vol);
            // Set color of new selected button to primaryColorVariant, set previous selected button back to primaryColor
            if (currentStation != null) {
              if (currentStation < n_rds_buttons) {
                rds_button_current = (Button) findViewById(rds_button_ids[currentStation]);
                Log.i(TAG, "current_station = " + currentStation + " rds_button_current_id = " + rds_button_ids[currentStation]);
                rds_button_current.setBackgroundColor(primaryColor);
              } else {
                preset_button_current = (Button) findViewById(preset_button_ids[currentStation - n_rds_buttons]);
                Log.i(TAG, "current_station = " + currentStation + " preset_button_current_id = " + preset_button_ids[currentStation - n_rds_buttons ]);
                preset_button_current.setBackgroundColor(primaryColor);
              }
            }
            rds_button = (Button) findViewById(button_id);
            rds_button.setBackgroundColor(primaryColorVariant);
            currentStation = button_clicked_index;
          }
        });
      }
    }

    @Override
    protected void onCancelled(){
      Log.i(TAG, "In onCancelled");
      queue.stop();
      System.exit(0);
    }
  } // End class BackgroundGetRDS

  private void sendGet(String url) {
    stringRequest = new StringRequest(Request.Method.GET, url, new Response.Listener<String>() {
      @Override
      public void onResponse(String response) {
        Log.i(TAG,"Get Response: " + response.toString());
        last_rds_info = response.toString();
      }
    }, new Response.ErrorListener() {
      @Override
      public void onErrorResponse(VolleyError error) {
        Log.i(TAG,"Get Response Error: " + error.toString());
      }
    });

    queue.add(stringRequest);
  }

  private void sendPost(String url, String freqvol_string) {
    stringRequest = new StringRequest(Request.Method.POST, url, new Response.Listener<String>() {
      @Override
      public void onResponse(String response) {
        Log.i(TAG,"Post Response: " + response.toString());
      }
    }, new Response.ErrorListener() {
      @Override
      public void onErrorResponse(VolleyError error) {
        Log.i("Info","Post Response Error: " + error.toString());
      }
    }){
      @Override
      protected Map<String, String> getParams() {
        Map<String, String> postData = new HashMap<String, String>();
        postData.put("freqvol", freqvol_string);
        return postData;
      }
    };

    queue.add(stringRequest);
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    int id = item.getItemId();

    if (id == R.id.action_settings) {
      Intent intent = new Intent(this, SettingsActivity.class);
      startActivityForResult(intent, ACTIVITY_SETTINGS);
      return true;
    }
    return super.onOptionsItemSelected(item);
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == ACTIVITY_SETTINGS) {
      if (resultCode == RESULT_OK) {
        if (data.getStringExtra("ip_address") != null){
          ip_address= data.getStringExtra("ip_address");
          Log.i(TAG, "Return from SettingsActivity ip_address=" + ip_address);
        }
        String[] temp_preset_freq = data.getStringArrayExtra("preset_freq");
        for (int i=0; i<n_preset_buttons; i++){
          if (temp_preset_freq[i] != null) {
            preset_freq[i] = temp_preset_freq[i];
            Log.i(TAG, "Return from SettingsActivity preset_freq[" + i + "]=" + preset_freq[i]);
            preset_button = (Button) findViewById(preset_button_ids[i]);
            preset_button.setText(preset_freq[i]);
          }
        }
      }
    }
  } // End OnActivityResult
}  // End MainActivity

FindStations

Java
Android app that communicates with Raspberry Pi - based radio to display station information after SEEK
Displays 20 strongest discovered stations and program types
Click on any button to play station
// DJ Radio 2
// Communicates with Raspberry Pi - based dual radio to display station information after SEEK
// Displays discovered stations and program types
// Click on any button to play station

package com.example.djradio2;

import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Build;

import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;

import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.AbsoluteSizeSpan;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;

import android.util.Log;
import android.util.TypedValue;

import android.widget.Button;
import android.widget.GridLayout;
import android.widget.Toast;

import com.android.volley.DefaultRetryPolicy;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.StringRequest;
import com.android.volley.toolbox.Volley;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

import static android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE;
import static java.lang.Thread.sleep;

public class MainActivity extends AppCompatActivity {

  static int n_rows = 5;
  static int n_cols = 4;
  private static final String TAG = MainActivity.class.getName();
  private RequestQueue queue;
  private StringRequest stringRequest;
  private String ip_address, url;

  private Button currentStation = null;
  private int primaryColor, primaryColorVariant, onPrimaryColor;

  Button[][] button_array;
  Integer[][] button_ids;

  String ip_address_default = "192.168.2.101";

  final static int ACTIVITY_SETTINGS = 0;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Toolbar toolbar = findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    // Find IP address of Raspberry Pi
    if (Build.FINGERPRINT.contains("generic"))
      ip_address = ip_address_default;   // Emulator
    else {   // Real device
      // Find IP address assigned to Raspberry Pi from Pixel Hotspot
      // ip neigh returns all IPs in neighborhood
      // One we want is 192.168.x.187 dev wlan1
      try {
        Process process = Runtime.getRuntime().exec("ip neigh");
        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        int read;
        char[] buffer = new char[4096];
        StringBuffer output = new StringBuffer();
        while ((read = reader.read(buffer)) > 0) {
          output.append(buffer, 0, read);
        }
        reader.close();
        process.waitFor();
        String out = output.toString();
        int end_ind = out.indexOf(" dev wlan1");
        if (end_ind != -1) {
          out = out.substring(0, end_ind);
          int start_ind = out.lastIndexOf("192");
          if (start_ind != -1)
          ip_address = out.substring(start_ind);
          else{
            Toast.makeText(getApplicationContext(), "Can't determine IP address, check Wifi enabled", Toast.LENGTH_LONG).show();
            ip_address = ip_address_default;
          }
        }
        else{
          Toast.makeText(getApplicationContext(), "Can't determine IP address, check wifi enabled", Toast.LENGTH_LONG).show();
          ip_address = ip_address_default;
        }
      } catch (IOException e) {
        throw new RuntimeException(e);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
    }
    Log.i(TAG, "ip address of Raspberry pi = " + ip_address);

    // Set colors from theme
    final TypedValue value = new TypedValue();
    getTheme().resolveAttribute(R.attr.colorPrimary, value, true);
    primaryColor = value.data;
    getTheme().resolveAttribute(R.attr.colorPrimaryVariant, value, true);
    primaryColorVariant = value.data;
    getTheme().resolveAttribute(R.attr.colorOnPrimary, value, true);
    onPrimaryColor = value.data;

    // Set up Start button
    Button startButton = findViewById(R.id.startButton);
    startButton.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        Log.i(TAG, "Start button clicked");
        // Send GET Request to start station seek program
        url = "http://" + ip_address + ":5000/find_stations";
        sendGet(url);
        try {
          sleep(1000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    });

    // Set up Exit button
    Button exitButton = findViewById(R.id.exitButton);
    exitButton.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        System.exit(0);
      }
    });

    // Initialize button array
    button_array = new Button[n_rows][n_cols];
    button_ids = new Integer[n_rows][n_cols];

    int display_width = Resources.getSystem().getDisplayMetrics().widthPixels;
    int display_height = Resources.getSystem().getDisplayMetrics().heightPixels - 450; // Leave room for taskbar and Exit button
    Log.i(TAG, "Usable display width, height = " + display_width + " x " + display_height);

    GridLayout button_grid_layout = findViewById(R.id.buttonArray);
    button_grid_layout.setRowCount(n_rows);
    button_grid_layout.setColumnCount(n_cols);
    for (int i = 0; i < n_rows; i++) {
      for (int j = 0; j < n_cols; j++) {
        button_array[i][j] = new Button(this);
        button_array[i][j].setId(10 * i + j);
        button_ids[i][j] = button_array[i][j].getId();
        GridLayout.LayoutParams params = new GridLayout.LayoutParams();
        params.width = Math.floorDiv(display_width, n_cols);
        params.height = Math.floorDiv(display_height, n_rows);
        Log.i(TAG, "Button width, height = " + params.width + " x " + params.height);
        params.rowSpec = GridLayout.spec(i);
        params.columnSpec = GridLayout.spec(j);
        button_array[i][j].setLayoutParams(params);
        button_grid_layout.addView(button_array[i][j]);
      }
    }

    // Start queue of network requests
    queue = Volley.newRequestQueue(this);

  } // End OnCreate

  private void sendGet(String url) {
    stringRequest = new StringRequest(Request.Method.GET, url, new Response.Listener<String>() {
      @Override
      public void onResponse(String response) {
        String stations = response.toString();
//      Log.i(TAG,"stations string= " + stations);
        String[] station_array = stations.split("\\|", 0);
        int n_stations = station_array.length - 1;  // Nothing after last | in string
        Log.i(TAG, "number of stations found=" + n_stations);
        if (n_stations > n_rows * n_cols) n_stations = n_rows * n_cols;  // Just in case
        for (int i_station=0; i_station<n_stations; i_station++) {
          Log.i(TAG, "station: " + station_array[i_station]);
          String freq = station_array[i_station].substring(0,5);
          String prog_type = station_array[i_station].substring(5);
          Log.i(TAG, "Station[" + i_station + "]:  Freq=" + freq + " Program Type=" + prog_type);
          int i = i_station / n_cols;  // Row
          int j = i_station % n_cols;  // Column
          button_array[i][j].setAllCaps(false);
          SpannableString f = new SpannableString(freq + '\n');
          SpannableString pty = new SpannableString(prog_type);
          f.setSpan(new AbsoluteSizeSpan(22, true), 0, freq.length(), SPAN_INCLUSIVE_INCLUSIVE);
          pty.setSpan(new AbsoluteSizeSpan(15, true), 0, pty.length(), SPAN_INCLUSIVE_INCLUSIVE);
          button_array[i][j].setText(TextUtils.concat(f, pty));
          button_array[i][j].setTextColor(onPrimaryColor);
          button_array[i][j].setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
              int button_id = view.getId();
              Log.i(TAG, "onClick button_id = " + button_id + ", position [" + button_id/10 + "][" + button_id % 10  + "]");
              String url = "http://" + ip_address + ":5000/playradio";
              String freq_fixed;
              if (freq.substring(0,1).equals(" ")) freq_fixed = freq.substring(1); else freq_fixed = freq;
              String freq_vol = freq_fixed + " 15";
              Log.i(TAG, "In onClick: url = " + url + " freq_vol = " + freq_vol);
              sendPost(url, freq_vol);
              if (currentStation != null) currentStation.setBackgroundTintList(getResources().getColorStateList(R.color.blue, getTheme())); //primaryColor
              button_array[i][j].setBackgroundTintList(getResources().getColorStateList(R.color.darkblue, getTheme()));  //primaryColorVariant
              currentStation = button_array[i][j];
            }
          });
        }
      }
    }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error){
        Log.i(TAG, "Get Response Error: " + error.toString());
      }
    });

    stringRequest.setRetryPolicy(new DefaultRetryPolicy(20000, 5, 1));
//    Log.i(TAG, "Retry policy:" +  stringRequest.getTimeoutMs() + " " +  stringRequest.getRetryPolicy());
    queue.add(stringRequest);
    Toast.makeText(getApplicationContext(), "Searching for stations...please wait", Toast.LENGTH_LONG).show();
  }

  private void sendPost(String url, String freqvol_string) {
    stringRequest = new StringRequest(Request.Method.POST, url, new Response.Listener<String>() {
      @Override
      public void onResponse(String response) {
        Log.i(TAG,"Post Response: " + response.toString());
      }
    }, new Response.ErrorListener() {
      @Override
      public void onErrorResponse(VolleyError error) {
        Log.i("Info","Post Response Error: " + error.toString());
      }
    }){
      @Override
      protected Map<String, String> getParams() {
        Map<String, String> postData = new HashMap<String, String>();
        postData.put("freqvol", freqvol_string);
        return postData;
      }
    };

    queue.add(stringRequest);
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    int id = item.getItemId();

    if (id == R.id.action_settings) {
      Intent intent = new Intent(this, SettingsActivity.class);
      startActivityForResult(intent, ACTIVITY_SETTINGS);
      return true;
    }
    return super.onOptionsItemSelected(item);
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == ACTIVITY_SETTINGS) {
      if (resultCode == RESULT_OK) {
        if (data.getStringExtra("ip_address") != null){
          ip_address= data.getStringExtra("ip_address");
          Log.i(TAG, "Return from SettingsActivity ip_address=" + ip_address);
        }
      }
    }
  } // End OnActivityResult
}  // End MainActivity

si4703_rds_to_file.py

Python
si4703_rds_to_file.py: scan specified frequencies, writing artist and title to one file and all info to second file
Syntax: python si4703_rds_to_file.py i2c_bus freqs_file
# Connects Silicon Labs Si4703 radio receiver (from Sparkfun kit WRL-12938) with Raspberry Pi 2 Model B v1.1
# si4703_rds_to_file.py - scan specified frequencies, writing artist and title to one file and all info to second file
# Syntax: python si4703_rds_to_file.py i2c_bus freqs_file

# Silicon Labs Documentation:
# Operating instructions: https://www.silabs.com/documents/public/application-notes/AN230.pdf (AN230)
# Registers: https://www.silabs.com/documents/public/data-sheets/Si4702-03-C19.pdf (Si4702-03-C19)

# Register read/write information from KansasCoder (see https://www.raspberrypi.org/forums/viewtopic.php?t=28920)
# Initialization based on Pi Flyer in thread at
#   https://www.raspberrypi.org/forums/viewtopic.php?f=41&t=28920&start=25&sid=eb677d15d2577540ab9e7f68ce76b35a

import sys
from os import path
import RPi.GPIO as GPIO
import smbus
from time import time, sleep
from datetime import datetime
import subprocess


def main():
  
  # Check correct number of arguments, read them
  if (len(sys.argv) != 3):
    print("Syntax: python si4703_rds_to_file.py i2c_bus freqs_file")
    exit(-1)
  else:
    i2c_bus = int(sys.argv[1])
    freqs_file = sys.argv[2]
    print("Scanning specified frequencies from file %s on radio on i2c bus %d" % (freqs_file, i2c_bus))
  
  # Check for frequencies file
  if not path.exists(freqs_file):
    print('Need file ' + freqs_file); exit()
  else:
    f_freqs = open(freqs_file, "r")

  initialize_si4703(i2c_bus)
  i2c = smbus.SMBus(i2c_bus)
  address = 0x10 #address of SI4703
  
  # Si4703 contains 32 8-bit integers, each corresponding to a byte of a 16-bit word:
  # reg = i2c.read_i2c_block_data(address, value, 32) returns them as:
  # 0A high, 0A low ... 0F high, 0F low, 00 high, 00 low .. 09 high, 09 low
  # We will combine high and low bytes since Silicon Labs register spec refers to entire 16-bit word
  # The mapping between a register number reg_no and the location of its high byte in reg is
  # ind = 2 * ((16 + (reg_no - 0xA)) % 16)

  # NOTE about register reads: 
  # i2c.read_i2c_block_data(address, value, 32) also WRITES value to Reg 2 high
  # as discovered by KansasCoder (see https://www.raspberrypi.org/forums/viewtopic.php?t=28920)
  # Thus we must always keep track of Reg 2 high, and then send it as i2c.read_i2c_block_data 2nd parameter
  # We will define 
  # shadow_register = reg[16:28]   # Reg 2 high, 2 low, ... 7 high, 7 low (only Reg 2 - 7 are writeable)
  # to keep track of the current value of the registers and read it before every write
  # So reads will use: 
  # reg = i2c.read_i2c_block_data(address, shadow_register[0], 32)
  # The function decode_reg(reg, reg_no, low_bit, high_bit) is used to pull a certain value from reg using the bits specified in the Si Labs spec 

  # NOTE about register writes:
  # i2c.write_i2c_block_data(address, value, byte_list) writes value to Reg 2 high and then the list to 2 low, 3 high, 3 low etc.
  # where value is an 8-bit byte (expressed as an integer) and byte_list is the list of 8-bit bytes
  # We will use shadow register described above for the write as follows:
  # i2c.write_i2c_block_data(address, shadow_register[0], shadow_register[1:])
  # So to do a write (this is done in function write_register):
  # reg = i2c.read_i2c_block_data(address, shadow_register[0], 32)             # Read current register
  # shadow_register = reg[16:28]                                               # Copy current register values 2 - 7 into shadow register
  # update_shadow_register(shadow_register, reg_no, low_bit, high_bit, value)  # Update shadow_register reg_no[low_bit:high_bit] with value 
  # i2c.write_i2c_block_data(address, shadow_register[0], shadow_register[1:]) # Write register

  # Follow AN230 Powerup Configuration Sequence (p. 12)
  # Set the XOSCEN bit (Reg 7 [15]) to power up the crystal
  shadow_register = [0] * 12    # Nothing in Reg 2 high set yet
  shadow_register = write_register(i2c, address, shadow_register, 7, 15, 15, 1)

  print("Powering up the crystal")
  #  Wait for crystal to power up (required for crystal oscillator operation), minimum 500 ms
  sleep(.5)
  print("Crystal now powered up")
  
  # Set DMUTE Reg 2 [14] to disable mute and ENABLE Reg 2 [0] to set the powerup state
  print("Powering up the device")
  shadow_register = write_register(i2c, address, shadow_register, 2, 14, 14, 1)
  shadow_register = write_register(i2c, address, shadow_register, 2, 0, 0, 1)
  #  Wait for device to powerup
  sleep(.1)
  print("Device now powered up")

  # Enable RDS (Reg 4 [12])
  # See https://www.silabs.com/documents/public/application-notes/AN243.pdf for RDS reception details
  # See http://www.interactive-radio-system.com/docs/EN50067_RDS_Standard.pdf for Radiotext (RT) details
  shadow_register = write_register(i2c, address, shadow_register, 4, 12, 12, 1)
  print("RDS Enabled")
 
  # Parse frequencies file
  # Format:
  # freqs = [93.7, 97.1, 98.1, 98.5, 98.9, 100.1, 100.7]
  # freqs_with_RT_plus = [96.7, 98.1, 100.7]             - optional
  freqs = []
  freqs_with_RT_plus = []
  freqs_line = f_freqs.readline()
  if (len(freqs_line) == 0):
    print('No frequencies specified')
    f_freqs.close()
    exit(-1)
  freqs = eval(freqs_line[freqs_line.index('['):])
  freqs_with_RT_plus_line = f_freqs.readline()
  if (len(freqs_with_RT_plus_line) != 0):
    freqs_with_RT_plus = eval(freqs_with_RT_plus_line[freqs_with_RT_plus_line.index('['):])
  print('Freqs to scan = ' + str(freqs))
  print('Freqs with RT_plus = ' + str(freqs_with_RT_plus))
  
# Write stations with artist and title to artist_title.txt and append data from all stations to all_stations.txt
  f_artist_title = open('/home/pi/si4703/artist_title.txt', 'w')
  f_all_stations = open('/home/pi/si4703/all_stations.txt', 'a')

  # Start scanning frequencies for RDS and RDS_plus info
  time_out_sec = 15
  while (True):
    for freq in freqs:
#     print('\n' + ('%5.1f' % (freq)))
      # To tune the desired station write channel to CHAN (Reg 3 [0-9]) and set TUNE (Reg 3 [15])
      # Channel is defined as an integer representing how many steps it is above minimum
      # FM regional frequency given regional spacing
      SPACE = .2       # US spacing between FM stations in MHz
      MIN_FREQ = 87.5  # US minimum FM frequency in MHz
      channel = round((freq - MIN_FREQ) / SPACE)
      shadow_register = write_register(i2c, address, shadow_register, 3, 0, 9, channel)
      shadow_register = write_register(i2c, address, shadow_register, 3, 15, 15, 1)
      sleep(1)
      # Unset TUNE (Reg 3 [15]) to be able to retune
      shadow_register = write_register(i2c, address, shadow_register, 3, 15, 15, 0)
      sleep(1)
      
      # Obtain Radio Text (RT)
      # RT (and other info) is sent in 16-bit words in RDSA, RDSB, RDSC, RDSD in reg C, D, E and F
      # RDSA and RDSB indicate the Program Identifier (PI) or station ID and Group Type Code 
      # (what type of data is being sent - RT, traffic, etc.)
      # The RT is read in 4-character segments from unicode characters from RDSC and RDSD
      # The RT segment index (RDSB (Reg D) [0-3]) segment 0 indicates the start of the RT
      # The end of the RT transmission is shown by either a carriage return (CR - decimal 13) as one of the 4 chars,
      # a blank line (4 decimal 32 chars), or reaching the 15th segment (max 64 chars in RT)
      s_prev = ''             # The previously decoded string
      RT = ''                 # The RT string being built
      seen_RT_seg0 = False    # Whether the RT segment 0 has been seen yet
      found_RT = False        # Whether we've found the RT string yet
      no_RT = False           # If no RT found before timeout
      RT_start_time = time()
      artist = ''
      title = ''
      prev_seg_ind = -1
      while(not found_RT):
        reg = i2c.read_i2c_block_data(address, shadow_register[0], 32)
        if (decode_reg(reg, 0x0D, 12, 15) == 0x2):    # Wait for RDSB (reg D) [12-15] to show Group Type 0x2 => Radio Text (Group 2A) 
          seg_ind = decode_reg(reg, 0x0D, 0, 3)           # RT segment index
#         print(seg_ind)
          if ((seg_ind != 0) and (not seen_RT_seg0)):
            if (time() - RT_start_time) > time_out_sec:   # Bail if no RT within time_out_sec seconds
#             print('No RT segment 0 found within ' + str(time_out_sec) + ' sec')
              no_RT = True
              break
            else:
              continue                                    # Wait for RT segment 0
          else:
            seen_RT_seg0 = True
            if (seg_ind == prev_seg_ind + 1):             # Next RT segment
              CH = decode_reg(reg, 0x0E, 8, 15)           # Pull 4 unicode chars as integers from RDSC and RDSD
              CL = decode_reg(reg, 0x0E, 0, 7)
              DH = decode_reg(reg, 0x0F, 8, 15)
              DL = decode_reg(reg, 0x0F, 0, 7)
              if (CH in range(32,127)): chrCH = chr(CH)   # Unicode character corresponding to integer
              else: chrCH = ' '                           # Replace non-printing chars with blanks
              if (CL in range(32,127)): chrCL = chr(CL)
              else: chrCL = ' '
              if (DH in range(32,127)): chrDH = chr(DH)
              else: chrDH = ' '
              if (DL in range(32,127)): chrDL = chr(DL)
              else: chrDL = ' '
              s = chrCH + chrCL + chrDH + chrDL
              if (s != s_prev):                           # If this is a new piece of the string add it to the RT
                RT = RT + s
              s_prev = s
#             print(CH, CL, DH, DL)
#             print(str(seg_ind) + s)
              if (CH == 13 or CL == 13 or DH == 13 or DL == 13 or seg_ind == 15 or (CH == 32 and CL == 32 and DH == 32 and DL == 32)):
                found_RT = True 
#               print(RT)
                prev_seg_ind = -1
              else:
                prev_seg_ind = seg_ind
            if (time() - RT_start_time) > time_out_sec:   # Bail if no RT within time_out_sec seconds
#             print('RT not complete within ' + str(time_out_sec) + ' sec')
              no_RT = True
              break
        elif (time() - RT_start_time) > time_out_sec:   # Bail if no Group 2A within time_out_sec seconds
#         print('No Group 2A found within ' + str(time_out_sec) + ' sec')
          no_RT = True
          break
        
      if (no_RT == False):
#       print("Time to find RadioText: %5.1f sec" % (time() - RT_start_time))
        # Now that we've found RT find RT+ data for those stations known to have it, 
        # which shows where in the RT string the song title and artist are
        # For RT_plus format see https://tech.ebu.ch/docs/techreview/trev_307-radiotext.pdf
        if (freq in freqs_with_RT_plus): 
          found_RT_plus = False       # Whether we've found the RT+ info
          RTplus_start_time = time()
          while (not found_RT_plus):
            reg = i2c.read_i2c_block_data(address, shadow_register[0], 32)
            # Group 3 indicates that this transmission includes the Applications Identification (AID) for an Open Data Applications (ODA)
            #   You can verify with: print(decode_reg(reg, 0x0D, 12, 15))
            # RDSD (reg F) = 0x4bd7 is the AID for RT+ and RDSB bits 4 - 1 = 0xc indicates ODA type RT+
            if ((hex(decode_reg(reg, 0x0F, 8, 15)) == '0x4b') and (hex(decode_reg(reg, 0x0F, 0, 7)) == '0xd7') and (hex(decode_reg(reg, 0x0D, 1, 4)) == '0xc')):
#             print('0x4bd7 seen')
              while(not found_RT_plus):
                reg = i2c.read_i2c_block_data(address, shadow_register[0], 32)
                # If the Group Type is 0xc then we have RT+ data
                if (hex(decode_reg(reg, 0x0D, 12, 15)) == '0xc'):
                  item_toggle_bit =  decode_reg(reg, 0x0D, 4, 4)       # Item toggle bit - toggled when new item starts - not used here
                  item_running_bit = decode_reg(reg, 0x0D, 3, 3)       # Item running bit - toggled when item is running - not used here
                  item1_type  = decode_reg(reg, 0x0E, 13, 15)          # Item 1 type:  4 = artist   1 = song title
                  item1_start = decode_reg(reg, 0x0E, 7, 12)
                  item1_end   = decode_reg(reg, 0x0E, 1, 6)
                  item2_type  = decode_reg(reg, 0x0F, 11, 15)          # Item 2 type:  4 = artist   1 = song title
                  item2_start = decode_reg(reg, 0x0F, 5, 10)
                  item2_end   = decode_reg(reg, 0x0F, 0, 4)
                  if (item1_type == 4):
                    artist = RT[item1_start : 1 + item1_start + item1_end]
                  elif (item2_type == 4):
                    artist = RT[item2_start : 1 + item2_start + item2_end]
                  if (item1_type == 1):
                    title = RT[item1_start : 1 + item1_start + item1_end]
                  elif (item2_type == 1):
                    title = RT[item2_start : 1 + item2_start + item2_end]
                  elif (item1_type != 1 and item1_type != 4 and item2_type != 1 and item1_type != 4):
#                   print('RT+ does not contain title or artist')
                    pass
                  found_RT_plus = True
            elif (time() - RTplus_start_time) > time_out_sec:   # Bail if no RT+ within time_out_sec seconds
#             print('No RT+ found within ' + str(time_out_sec) + ' sec')
              break
        else:    # Parse artist and title from RT if they're not in RT+
          # Search RT for "by" or "/", split into title and artist
          if (RT.find('by') > -1):
            title = RT[:RT.find('by')-1]
            artist = RT[RT.find('by')+3:]
            # Strip out "now" or "on" from artist
            if (artist.find(' now ') > -1):
              artist = artist[:artist.find(' now ')]
            if (artist.find(' on ') > -1):
              artist = artist[:artist.find(' on ')]
            # Strip out "Now Playing" from title
            if (title.find('Now Playing ') > -1):
              title = title[title.find('Now Playing ')+12:]
          elif (RT.find('/') > -1):
            artist = RT[:RT.find('/')]
            title = RT[RT.find('/')+2:]
            # Strip out "now" or "on" from artist
            if (artist.find(' now ') > -1):
              artist = artist[:artist.find(' now ')]
            if (artist.find(' on ') > -1):
              artist = artist[:artist.find(' on ')]
            # Strip out "Now Playing" from title
            if (title.find('Now Playing ') > -1):
              title = title[title.find('Now Playing ')+12:]
  
      # Open favorites file. If artist is in it, add a * to the line
      if not path.exists('/home/pi/si4703/favorites.txt'):
        print('Need file /home/pi/si4703/favorites.txt'); exit()
      else:
        f_favorites = open('/home/pi/si4703/favorites.txt', "r")
      favorite_artists = []
      for line in f_favorites:
        favorite_artists.append(line.rstrip('\n'))
      f_favorites.close()

      # Output info
      if (RT == ''):
        output = ('%5.1f' % (freq))
      elif ((artist == '') and (title == '')):
        output = ('%5.1f' % (freq)) + '  ' + RT
      elif ((artist != '') and (title == '')):
        output = ('%5.1f' % (freq)) + '  ' + artist
      elif ((artist != '') and (title != '')):
        output = ('%5.1f' % (freq)) + '  ' + artist + '   ' + title
        if (artist in favorite_artists):
          output = output + ' *'
        f_artist_title.write(output + '\n')
        f_artist_title.flush()
      f_all_stations.write(datetime.now().strftime("%A %D %H:%M:%S") + ':  ' + output + '\n')
      f_all_stations.flush()
# End of main()
  
def initialize_si4703(bus):
  # Initialization based on Pi Flyer in thread at 
  #   https://www.raspberrypi.org/forums/viewtopic.php?f=41&t=28920&start=25&sid=eb677d15d2577540ab9e7f68ce76b35a
  # To add a 2nd i2c bus, see
  #   https://www.instructables.com/Raspberry-PI-Multiple-I2c-Devices/
  #
  # Wiring chart: 
  # Bus 1
  # SDA GPIO   (wired to Si4703 SDIO)  BCM GPIO 2 (physical pin 3)
  # SCL GPIO   (wired to Si4703 SCLK)  BCM GPIO 3 (physical pin 5)
  # Reset GPIO (wired to Si4703 ~RST)  BCM GPIO 4 (physical pin 7)
  # Bus 3
  # SDA GPIO   (wired to Si4703 SDIO)  BCM GPIO 23 (physical pin 16)
  # SCL GPIO   (wired to Si4703 SCLK)  BCM GPIO 24 (physical pin 18)
  # Reset GPIO (wired to Si4703 ~RST)  BCM GPIO 25 (physical pin 22)

  if (bus == 1):
    sda_gpio = 2
    scl_gpio = 3
    rst_gpio = 4
  if (bus == 3):
    sda_gpio = 23
    scl_gpio = 24
    rst_gpio = 25

  # Set the GPIO module to reference BCM pin numbers
  GPIO.setmode(GPIO.BCM)
  GPIO.setwarnings(False)
  
  # Setup Reset GPIO as output
  GPIO.setup(rst_gpio, GPIO.OUT)
  
  # Setup SDA GPIO as output
  # It needs to temporarily act as output to set the Si4703 to 2-wire (i2c) mode
  # Then we will set it back to i2c SDA
  GPIO.setup(sda_gpio, GPIO.OUT)
  
  # The Si4703 requires the following sequence to set it's busmode to 2-wire (i2c): 
  # Hold the SDIO pin low while the RST pin transitions from low to high
  GPIO.output(sda_gpio, GPIO.LOW) 
  sleep(.1) #allow pin to settle
  GPIO.output(rst_gpio, GPIO.LOW)
  sleep(.1)
  GPIO.output(rst_gpio, GPIO.HIGH)
  sleep(.1)
  
  # Setup SDA GPIO back to i2c SDA line
  # Make use of the subprocess module to execute the gpio program included
  # with WiringPi from the Raspbian commandline.
  # 'gpio' is the program to be executed, and the rest are options
  # '-g' is a flag that causes pin numbers to be interpreted as BCM_GPIO
  # 'mode' is the option used to select the mode of the pin
  # 'sda_gpio' is the pin to change the mode of
  # 'ALT0' is the alternate pin mode code for i2c 
  subprocess.check_output(['gpio', '-g', 'mode', str(sda_gpio), 'ALT0'])

def decode_reg(reg, reg_no, low_bit, high_bit):
  # Added by Dave Jaffe
  # get latest set of register values with: reg = i2c.read_i2c_block_data(address, value, 32)
  # Use low_bit, high_bit from Si4702-03-C19 spec
  # For example: CHANNEL (Reg 3 [0-9]) would be decoded from reg: decode_reg(reg, 3, 0, 9)
  # We will combine high and low bytes since Silicon Labs register spec refers to entire 16-bit word

  # reg byte layout: 0A high, 0A low ... 0F high, 0F low, 00 high, 00 low .. 09 high, 09 low
  # The mapping between a register number reg_no and the location of its high byte in reg is
  ind = 2 * ((16 + (reg_no - 0xA)) % 16)
  H = reg[ind]      # Register high byte
  L = reg[ind + 1]  # Register low byte
  W = 256*H + L     # Register 16-bit word
  # Use bit mask to pull desired bits, return them as an integer
  mask_len = high_bit - low_bit + 1
  mask = 2**mask_len - 1
  return (((mask << low_bit) & W) >> low_bit)
 
def update_shadow_register(shad_reg, reg_no, low_bit, high_bit, value):
  # Added by Dave Jaffe
  # Set shadow_register values
  # Use low_bit, high_bit from Si4702-03-C19 spec
  # For example, to set CHANNEL (Reg 3 [0-9]) update_shadow_register(shadow_register, 3, 0, 9, channel)
  # We will combine high and low bytes since Silicon Labs register spec refers to entire 16-bit word

  # shadow_register layout: Reg 2 high, 2 low, ... 7 high, 7 low (only Reg 2 - 7 are writeable)
  # The mapping between a register number reg_no and the location of its high byte in shadow_register is
  ind = (reg_no-2) * 2
  H = shad_reg[ind]      # Register high byte
  L = shad_reg[ind + 1]  # Register low byte
  W = 256*H + L     # Register 16-bit word
# print(format(W, '016b'))
  # First, set bits where new value will go to 0 with a negative mask
  mask_len = high_bit - low_bit + 1
  mask = 2**mask_len - 1
  mask = mask << low_bit
  W = W & ~mask
  # Next, move new value to proper bit and add it to W
  V = value << low_bit
  W = W  + V
  # Break up W into H and L and write to shadow_register
  H = W >> 8
  L = W & 2**8 - 1 
  shad_reg[ind] = H
  shad_reg[ind + 1] = L

def write_register(i2c, address, shad_reg, reg_no, low_bit, high_bit, value):
  reg = i2c.read_i2c_block_data(address, shad_reg[0], 32)
  shad_reg = reg[16:28] 
  update_shadow_register(shad_reg, reg_no, low_bit, high_bit, value)
  i2c.write_i2c_block_data(address, shad_reg[0], shad_reg[1:])
  return (shad_reg)

if __name__ == "__main__":
  main()

si4703_rest_server.py

Python
si4703_rest_server.py: REST API server front end for si4703 apps
Enables reading/specifying current frequency/volume through get/post
To read: curl 192.168.1.104:5000/playradio
To spec: curl -d "freqvol=100.1 10" -X POST 192.168.1.104:5000/playradio
# si4703_rest_server.py: REST API server front end for si4703 apps
# Enables reading/specifying current frequency/volume through get/post
# To read:  curl 192.168.1.104:5000/playradio
# To spec:  curl -d "freqvol=100.1 10" -X POST 192.168.1.104:5000/playradio

# To run
# In OS:
#   In one shell: python si4703_player.py 1
#   In 2nd shell:
#     export FLASK_APP=si4703_rest_server.py
#     export FLASK_DEBUG=1  # If desired
#     flask run --host=0.0.0.0
#
# or include in /etc/rc.local:
#   export FLASK_APP=/home/pi/si4703/si4703_rest_server.py
#   /usr/bin/flask run --host=0.0.0.0 &
#   /usr/bin/python3 /home/pi/si4703/si4703_player.py 1 &

from flask import Flask, request
from os import path, system
from subprocess import Popen, run, PIPE
from time import sleep
app = Flask(__name__)

@app.route('/playradio', methods=['POST', 'GET'])
def playradio():
  if request.method == 'GET':
    if not path.exists("/home/pi/si4703/freq_vol.txt"):
      return 'freq_vol.txt does not exist\n'
    else:
      f_freq_vol = open("/home/pi/si4703/freq_vol.txt", "r")
      freq_vol = f_freq_vol.readline()
      f_freq_vol.close()
      space_ind = freq_vol.find(' ')
      freq = float(freq_vol[:space_ind])
      volume = int(freq_vol[space_ind+1:])
      return('Currently playing station %5.1f at volume %d\n' % (freq, volume))
  elif (request.method == 'POST'):
    f_freq_vol = open("/home/pi/si4703/freq_vol.txt", "w")
#   for k in request.form.keys():
#     print(k)
    freq_vol = request.form['freqvol']
    freq_vol = freq_vol.lstrip()  # Remove leading blank if any
    space_ind = freq_vol.find(' ')
    freq = float(freq_vol[:space_ind])
    volume = int(freq_vol[space_ind+1:])
    f_freq_vol.write(freq_vol)
    f_freq_vol.close()
    return('Now playing station %5.1f at volume %d\n' % (freq, volume))
  else:
    return 'Only GET or POST supported\n'

@app.route('/startrds', methods=['GET'])
def startrds():
  # Check to make sure si4703_rds_to_file.py is not already running
  ps = Popen(['ps', 'aux'], stdout=PIPE).communicate()[0]
  pss = ps.decode("utf-8")
  if (pss.find('si4703_rds_to_file') > -1):
    return 'si4703_rds_to_file.py already running\n'
  elif not path.exists("/home/pi/si4703/si4703_rds_to_file.py"):
    return 'Program si4703_rds_to_file.py does not exist\n'
  elif not path.exists("/home/pi/si4703/freqs_rds.txt"):
    return 'freqs_rds.txt does not exist\n'
  else:
    Popen(['/usr/bin/python3', '/home/pi/si4703/si4703_rds_to_file.py', '3', '/home/pi/si4703/freqs_rds.txt'])
    return('RDS scan started\n')

@app.route('/getrds', methods=['GET'])
def getrds():
  if not path.exists("/home/pi/si4703/artist_title.txt"):
    return 'artist_title.txt does not exist\n'
  else:
    f_artist_title = open("/home/pi/si4703/artist_title.txt", "r")
    freq_artist_title = ''
    while(True):
      line = f_artist_title.readline()
      if not line:
        break
      else:
        freq_artist_title = line
    return(freq_artist_title)

@app.route('/find_stations', methods=['GET'])
def startseek():
  run(['/usr/bin/python3', '/home/pi/si4703/si4703_seek.py', '3'])
  f_stations = open("/home/pi/si4703/stations.txt", "r")
  line = f_stations.readline()
  return(line)

si4703_seek.py

Python
si4703_seek.py: finds all working FM channels
Syntax: python si4703_seek.py i2c_bus
# Connects Silicon Labs Si4703 radio receiver (from Sparkfun kit WRL-12938) with Raspberry Pi 2 Model B v1.1
# si4703_seek.py: finds all working FM channels
# Syntax: python si4703_seek.py i2c_bus 

# Silicon Labs Documentation:
# Operating instructions: https://www.silabs.com/documents/public/application-notes/AN230.pdf (AN230)
# Registers: https://www.silabs.com/documents/public/data-sheets/Si4702-03-C19.pdf (Si4702-03-C19)

# Register read/write information from KansasCoder (see https://www.raspberrypi.org/forums/viewtopic.php?t=28920)
# Initialization based on Pi Flyer in thread at
#   https://www.raspberrypi.org/forums/viewtopic.php?f=41&t=28920&start=25&sid=eb677d15d2577540ab9e7f68ce76b35a

import sys
import RPi.GPIO as GPIO
import smbus
from time import time, sleep
import subprocess
from os import path

def main():
  
  # Check correct number of arguments, read them
  if (len(sys.argv) != 2): 
    print("Syntax: python si4703_seek.py i2c_bus")
    exit(-1)
  else:
    i2c_bus = int(sys.argv[1])
    print("Seeking all channels on radio on i2c bus %d" % (i2c_bus))
  
  initialize_si4703(i2c_bus)
  i2c = smbus.SMBus(i2c_bus)
  address = 0x10 #address of SI4703
  
  # Si4703 contains 32 8-bit integers, each corresponding to a byte of a 16-bit word:
  # reg = i2c.read_i2c_block_data(address, value, 32) returns them as:
  # 0A high, 0A low ... 0F high, 0F low, 00 high, 00 low .. 09 high, 09 low
  # We will combine high and low bytes since Silicon Labs register spec refers to entire 16-bit word
  # The mapping between a register number reg_no and the location of its high byte in reg is
  # ind = 2 * ((16 + (reg_no - 0xA)) % 16)

  # NOTE about register reads: 
  # i2c.read_i2c_block_data(address, value, 32) also WRITES value to Reg 2 high
  # as discovered by KansasCoder (see https://www.raspberrypi.org/forums/viewtopic.php?t=28920)
  # Thus we must always keep track of Reg 2 high, and then send it as i2c.read_i2c_block_data 2nd parameter
  # We will define 
  # shadow_register = reg[16:28]   # Reg 2 high, 2 low, ... 7 high, 7 low (only Reg 2 - 7 are writeable)
  # to keep track of the current value of the registers and read it before every write
  # So reads will use: 
  # reg = i2c.read_i2c_block_data(address, shadow_register[0], 32)
  # The function decode_reg(reg, reg_no, low_bit, high_bit) is used to pull a certain value from reg using the bits specified in the Si Labs spec 

  # NOTE about register writes:
  # i2c.write_i2c_block_data(address, value, byte_list) writes value to Reg 2 high and then the list to 2 low, 3 high, 3 low etc.
  # where value is an 8-bit byte (expressed as an integer) and byte_list is the list of 8-bit bytes
  # We will use shadow register described above for the write as follows:
  # i2c.write_i2c_block_data(address, shadow_register[0], shadow_register[1:])
  # So to do a write (this is done in function write_register):
  # reg = i2c.read_i2c_block_data(address, shadow_register[0], 32)             # Read current register
  # shadow_register = reg[16:28]                                               # Copy current register values 2 - 7 into shadow register
  # update_shadow_register(shadow_register, reg_no, low_bit, high_bit, value)  # Update shadow_register reg_no[low_bit:high_bit] with value 
  # i2c.write_i2c_block_data(address, shadow_register[0], shadow_register[1:]) # Write register

# print("Initial Register Readings")
# reg = i2c.read_i2c_block_data(address, 0, 32)  # Nothing in Reg 2 high set yet
# print("Register 00h - Device ID Part Number: " + hex(decode_reg(reg, 0x00, 12, 14)))
# print("Register 00h - Device ID Manufacturer ID: " + hex(decode_reg(reg, 0x00, 0, 11)))

  # Follow AN230 Powerup Configuration Sequence (p. 12)
  # Set the XOSCEN bit (Reg 7 [15]) to power up the crystal
  shadow_register = [0] * 12    # Nothing in Reg 2 high set yet
  shadow_register = write_register(i2c, address, shadow_register, 7, 15, 15, 1)

  print("Powering up the crystal")
  #  Wait for crystal to power up (required for crystal oscillator operation), minimum 500 ms
  sleep(.5)
  print("Crystal now powered up")
  
  # Set DMUTE Reg 2 [14] to disable mute and ENABLE Reg 2 [0] to set the powerup state
  print("Powering up the device")
  shadow_register = write_register(i2c, address, shadow_register, 2, 14, 14, 1)
  shadow_register = write_register(i2c, address, shadow_register, 2, 0, 0, 1)
  #  Wait for device to powerup
  sleep(.1)
  print("Device now powered up")

  # Enable RDS (Reg 4 [12])
  shadow_register = write_register(i2c, address, shadow_register, 4, 12, 12, 1)
  print("RDS Enabled")
   
  # To set up for the scan we need to set SEEKUP (Reg 2 [9]) to make scan direction up
  # and SKMODE (Reg 2 [10]) to stop scan when it reaches top frequency
  shadow_register = write_register(i2c, address, shadow_register, 2, 9, 9, 1)
  shadow_register = write_register(i2c, address, shadow_register, 2, 10, 10, 1)
  
  # Now turn seek on by setting SEEK (Reg 2 [8]) and monitoring STC (Scan/Tune Complete, Reg A [14])
  # and SF/BL (Seek Fail/Band Limit, Reg A [13]), which indicates end of band has been reached
  # Check ST (Reg A [8]) for stereo
  
  # CHANNEL (Reg 03h [9:0]) is an integer representing how many steps it is above minimum FM regional frequency given regional spacing
  SPACE = .2       # US spacing between FM stations in MHz
  MIN_FREQ = 87.5  # US minimum FM frequency in MHz
  
  shadow_register = write_register(i2c, address, shadow_register, 2, 8, 8, 1)

  # Dict of select program types
  prog_types = {1:'News', 3:'Sports', 4:'Talk', 5:'Rock', 6:'ClassicRock', 7:'AdultHits', 8:'SoftRock', 9:'Top40', 10:'Country', 11:'Oldies'}
  prog_types.update({12:'SoftMusic', 14:'Jazz', 15:'Classical', 16:'Rhythm&Blues', 17:'SoftRhythm&Blues', 19:'ReligiousMusic', 25:'SpanishMusic', 26:'HipHop'})

  f_stations_file = open('/home/pi/si4703/stations.txt', 'w')
  stations_dict = {}
  topN = 20  # Number of strongest stations to return

  while (True):
    sleep(0.05)
    reg = i2c.read_i2c_block_data(address, shadow_register[0], 32)
    if (decode_reg(reg, 0x0A, 13, 13) == 1):    # Reached end of band
      print('End of band reached. %d stations found. Top %d output to stations.txt' % (len(stations_dict), topN))
      # Sort by RSSI with largest first - returns a list
      strongest_stations = sorted(stations_dict.items(), key=lambda x: x[1], reverse=True)[:topN]
      # Turn list back into dict and then create a list just from the keys
      strongest_stations_dict = dict(strongest_stations) 
      strongest_stations_cp = list(strongest_stations_dict.keys())
      # Sort by channel with lowest alphabetical (eg. (space)88.7 first, 107.9 last)  
      strongest_stations_cp.sort()
      for ss in strongest_stations_cp: 
        f_stations_file.write(ss + '|')
      f_stations_file.write('\n')
      break
    if (decode_reg(reg, 0x0A, 14, 14) == 1):    # Found a station with a signal
      sleep(.2)  # Wait for PTY to catch up and then read registers again
      reg = i2c.read_i2c_block_data(address, shadow_register[0], 32)
      RSSI = decode_reg(reg, 0x0A, 0, 7)        # Signal strength (Reg A [0-7])
      if (RSSI > 8):                           # Filter out weak stations
        channel = decode_reg(reg, 0x0B, 0, 9) * SPACE + MIN_FREQ
        stereo_mono = ['Mono', 'Stereo'][decode_reg(reg, 0x0A, 8, 8)]
        PTY = decode_reg(reg, 0x0D, 5, 9)   # Radio Program Type field
        prog_type = 'Unknown'
        if (PTY in prog_types): 
          prog_type = prog_types[PTY] 
        print('Channel %.1f is %s with program type %s and signal strength %d' % (channel, stereo_mono, prog_type, RSSI))
        channel_prog_type = '%5.1f%s' % (channel, prog_type)
        stations_dict[channel_prog_type] = RSSI
      # Unset and reset SEEK
      shadow_register = write_register(i2c, address, shadow_register, 2, 8, 8, 0)
      sleep(0.1)
      shadow_register = write_register(i2c, address, shadow_register, 2, 8, 8, 1)

def initialize_si4703(bus):
  # Initialization based on Pi Flyer in thread at 
  #   https://www.raspberrypi.org/forums/viewtopic.php?f=41&t=28920&start=25&sid=eb677d15d2577540ab9e7f68ce76b35a
  # To add a 2nd i2c bus, see
  #   https://www.instructables.com/Raspberry-PI-Multiple-I2c-Devices/
  #
  # Wiring chart: 
  # Bus 1
  # SDA GPIO   (wired to Si4703 SDIO)  BCM GPIO 2 (physical pin 3)
  # SCL GPIO   (wired to Si4703 SCLK)  BCM GPIO 3 (physical pin 5)
  # Reset GPIO (wired to Si4703 ~RST)  BCM GPIO 4 (physical pin 7)
  # Bus 3
  # SDA GPIO   (wired to Si4703 SDIO)  BCM GPIO 23 (physical pin 16)
  # SCL GPIO   (wired to Si4703 SCLK)  BCM GPIO 24 (physical pin 18)
  # Reset GPIO (wired to Si4703 ~RST)  BCM GPIO 25 (physical pin 22)

  if (bus == 1):
    sda_gpio = 2
    scl_gpio = 3
    rst_gpio = 4
  if (bus == 3):
    sda_gpio = 23
    scl_gpio = 24
    rst_gpio = 25

  # Set the GPIO module to reference BCM pin numbers
  GPIO.setmode(GPIO.BCM)
  GPIO.setwarnings(False)
  
  # Setup Reset GPIO as output
  GPIO.setup(rst_gpio, GPIO.OUT)
  
  # Setup SDA GPIO as output
  # It needs to temporarily act as output to set the Si4703 to 2-wire (i2c) mode
  # Then we will set it back to i2c SDA
  GPIO.setup(sda_gpio, GPIO.OUT)
  
  # The Si4703 requires the following sequence to set it's busmode to 2-wire (i2c): 
  # Hold the SDIO pin low while the RST pin transitions from low to high
  GPIO.output(sda_gpio, GPIO.LOW) 
  sleep(.1) #allow pin to settle
  GPIO.output(rst_gpio, GPIO.LOW)
  sleep(.1)
  GPIO.output(rst_gpio, GPIO.HIGH)
  sleep(.1)
  
  # Setup SDA GPIO back to i2c SDA line
  # Make use of the subprocess module to execute the gpio program included
  # with WiringPi from the Raspbian commandline.
  # 'gpio' is the program to be executed, and the rest are options
  # '-g' is a flag that causes pin numbers to be interpreted as BCM_GPIO
  # 'mode' is the option used to select the mode of the pin
  # 'sda_gpio' is the pin to change the mode of
  # 'ALT0' is the alternate pin mode code for i2c 
  subprocess.check_output(['gpio', '-g', 'mode', str(sda_gpio), 'ALT0'])

def decode_reg(reg, reg_no, low_bit, high_bit):
  # Added by Dave Jaffe
  # get latest set of register values with: reg = i2c.read_i2c_block_data(address, value, 32)
  # Use low_bit, high_bit from Si4702-03-C19 spec
  # For example: CHANNEL (Reg 3 [0-9]) would be decoded from reg: decode_reg(reg, 3, 0, 9)
  # We will combine high and low bytes since Silicon Labs register spec refers to entire 16-bit word

  # reg byte layout: 0A high, 0A low ... 0F high, 0F low, 00 high, 00 low .. 09 high, 09 low
  # The mapping between a register number reg_no and the location of its high byte in reg is
  ind = 2 * ((16 + (reg_no - 0xA)) % 16)
  H = reg[ind]      # Register high byte
  L = reg[ind + 1]  # Register low byte
  W = 256*H + L     # Register 16-bit word
  # Use bit mask to pull desired bits, return them as an integer
  mask_len = high_bit - low_bit + 1
  mask = 2**mask_len - 1
  return (((mask << low_bit) & W) >> low_bit)
 
def update_shadow_register(shad_reg, reg_no, low_bit, high_bit, value):
  # Added by Dave Jaffe
  # Set shadow_register values
  # Use low_bit, high_bit from Si4702-03-C19 spec
  # For example, to set CHANNEL (Reg 3 [0-9]) update_shadow_register(shadow_register, 3, 0, 9, channel)
  # We will combine high and low bytes since Silicon Labs register spec refers to entire 16-bit word

  # shadow_register layout: Reg 2 high, 2 low, ... 7 high, 7 low (only Reg 2 - 7 are writeable)
  # The mapping between a register number reg_no and the location of its high byte in shadow_register is
  ind = (reg_no-2) * 2
  H = shad_reg[ind]      # Register high byte
  L = shad_reg[ind + 1]  # Register low byte
  W = 256*H + L     # Register 16-bit word
# print(format(W, '016b'))
  # First, set bits where new value will go to 0 with a negative mask
  mask_len = high_bit - low_bit + 1
  mask = 2**mask_len - 1
  mask = mask << low_bit
  W = W & ~mask
  # Next, move new value to proper bit and add it to W
  V = value << low_bit
  W = W  + V
  # Break up W into H and L and write to shadow_register
  H = W >> 8
  L = W & 2**8 - 1 
  shad_reg[ind] = H
  shad_reg[ind + 1] = L

def write_register(i2c, address, shad_reg, reg_no, low_bit, high_bit, value):
  reg = i2c.read_i2c_block_data(address, shad_reg[0], 32)
  shad_reg = reg[16:28] 
  update_shadow_register(shad_reg, reg_no, low_bit, high_bit, value)
  i2c.write_i2c_block_data(address, shad_reg[0], shad_reg[1:])
  return (shad_reg)

if __name__ == "__main__":
  main()

Credits

Dave Jaffe

Dave Jaffe

1 project • 2 followers
Longtime IT industry guy, loves to mess around with hardware and software.

Comments