Wiki2
BirdsClock (Rev #7)

Birds Clock

What doesn’t work

Browse filter doesn’t allow the song to played. Player just says Unable to Play Bofink.

SetBrowseFilterAlbum Bird Sounds
ListSongs
PlayIndex 0

SetBrowseFilterAlbum Bird Sounds
ListSongs
QueueAndPlay 0

SearchSongs Bofink
QueueAndPlay 0

Working session with playlist

This is an example telnet session towards SoundBridge using a playlist.

Don’t know how to easily add a playlist to minidlna.

$ telnet dream 5555
Trying 192.168.0.200...
Connected to dream.lounge.se.
Escape character is '^]'.
roku: ready
ListServers
ListServers: ListResultSize 2
ListServers: Music Library
ListServers: Internetradio
ListServers: ListResultEnd
ServerConnect 0
ServerConnect: TransactionInitiated
ServerConnect: Connected
ServerConnect: TransactionComplete
ListPlaylists
ListPlaylists: TransactionInitiated
ListPlaylists: ListResultSize 5
ListPlaylists: 60's Music
ListPlaylists: AAC Files
ListPlaylists: Birds
ListPlaylists: Non-DRMed Music
ListPlaylists: Recently Added
ListPlaylists: ListResultEnd
ListPlaylists: TransactionComplete
ListPlaylistSongs 2
ListPlaylistSongs: TransactionInitiated
ListPlaylistSongs: ListResultSize 14
ListPlaylistSongs: Track 67
ListPlaylistSongs: Track 19
ListPlaylistSongs: Track 25
ListPlaylistSongs: Track 32
ListPlaylistSongs: Track 53
ListPlaylistSongs: Track 54
ListPlaylistSongs: Track 70
ListPlaylistSongs: Track 83
ListPlaylistSongs: Track 55
ListPlaylistSongs: Track 92
ListPlaylistSongs: Track 86
ListPlaylistSongs: Track 24
ListPlaylistSongs: Track 68
ListPlaylistSongs: Birds
ListPlaylistSongs: ListResultEnd
ListPlaylistSongs: TransactionComplete
QueueAndPlayOne 0
QueueAndPlayOne: OK
QueueAndPlayOne 0
QueueAndPlayOne: OK
GetTransportState
GetTransportState: Play
GetTransportState
GetTransportState: Stop
ServerDisconnect
ServerDisconnect: TransactionInitiated
ServerDisconnect: Disconnected
ServerDisconnect: TransactionComplete
GetPowerState
GetPowerState: on
SetPowerState standby
SetPowerState: OK
exit

Working session with browsing

The preferred way towards a DLNA server seems to be through container browsing. The following session works.

$ telnet dream 5555
Trying 192.168.0.200...
Connected to dream.lounge.se.
Escape character is '^]'.
roku: ready
GetPowerState
GetPowerState: standby
SetPowerState on yes
SetPowerState: OK
GetConnectedServer
GetConnectedServer: OK
GetActiveServerInfo
GetActiveServerInfo: Type: upnp
GetActiveServerInfo: Name: OpenNAS DLNA Server
GetActiveServerInfo: OK

ListServers
ListServers: ListResultSize 2
ListServers: OpenNAS DLNA Server
ListServers: Internet Radio
ListServers: ListResultEnd
ServerConnect 0
ServerConnect: ConnectionFailedAlreadyConnected

GetCurrentContainerPath
GetCurrentContainerPath: /Album/Bird Sounds/

ContainerExit
ContainerExit: OK
ContainerExit
ContainerExit: OK
GetCurrentContainerPath
GetCurrentContainerPath: /

ListContainerContents
ListContainerContents: TransactionInitiated
ListContainerContents: ListResultSize 6
ListContainerContents: Album
ListContainerContents: All Music
ListContainerContents: Artist
ListContainerContents: Folders
ListContainerContents: Genre
ListContainerContents: Playlists
ListContainerContents: ListResultEnd
ListContainerContents: TransactionComplete
ContainerEnter 0
ContainerEnter: OK

ListContainerContents
ListContainerContents: TransactionInitiated
ListContainerContents: ListResultSize 62
ListContainerContents: 9 Lives to Wonder
ListContainerContents: A Hundred Days Off
ListContainerContents: All The King's Men
ListContainerContents: Amethyst Rock Star
ListContainerContents: Animositisomina
ListContainerContents: B-Sides Collection
ListContainerContents: Badmotorfinger
ListContainerContents: Beatles Cover - Rare!
ListContainerContents: Beaucoup Fish
ListContainerContents: Belief
ListContainerContents: Big Hit
ListContainerContents: Bird Sounds
ListContainerContents: Bites
...
ListContainerContents: Youth Novels
ListContainerContents: ListResultEnd
ListContainerContents: TransactionComplete
ContainerEnter 11
ContainerEnter: OK
ListContainerContents
ListContainerContents: TransactionInitiated
ListContainerContents: ListResultSize 13
ListContainerContents: Sädesärla
ListContainerContents: Rödhake
ListContainerContents: Näktergal
ListContainerContents: Koltrast
ListContainerContents: Grönsångare
ListContainerContents: Gransångare
ListContainerContents: Lövsångare
ListContainerContents: Blåmes
ListContainerContents: Talgoxe
ListContainerContents: Trädkrypare
ListContainerContents: Bofink
ListContainerContents: Grönfink
ListContainerContents: Kattuggla
ListContainerContents: ListResultEnd
ListContainerContents: TransactionComplete
QueueAndPlayOne 0     
QueueAndPlayOne: OK
GetTransportState
GetTransportState: Play
GetTransportState
GetTransportState: Stop
SetPowerState standby yes   
SetPowerState: OK
exit

A working bird clock script

So this script was what I finally came up with. There were a lot of issues because the telnet API isn’t perfect, but this script works fine.


#!/usr/local/bin/ruby

# - #!/usr/bin/env ruby

require 'net/telnet'

# Other communicaations:
# Web UI: http://soundbridge.local./SoundBridgeStatus.html
# Shell: telnet dream 4444

# Other sounds: Car sounds, gingles, TV shows, Cities, African animals ...

# Run from cron:
# 0     8-22     *     *     *         /root/bin/birdclock.sh

SERVER = "OpenNAS DLNA Server"
PATH = [ "Album", "Bird Sounds" ]
ITEMS = [ "Sädesärla","Rödhake","Näktergal","Koltrast","Grönsångare","Gransångare",
          "Lövsångare","Blåmes","Talgoxe","Trädkrypare","Bofink","Grönfink","Kattuggla" ]
DAYS = [ "Måndag","Tisdag","Onsdag","Torsdag","Fredag","Lördag","Söndag" ]

def connect
  @sb = Net::Telnet::new( "Host" => "dream", "Port" => 5555, "Timeout" => 10 )
  @sh = Net::Telnet::new( "Host" => "dream", "Port" => 4444, "Timeout" => 10 )
end

def quit_session(code = 0, leave_on = false)
  turn_off unless leave_on
  call("exit", /SoundBridge> /, @sh)
  @sh.close
  @sb.close
  exit code
end

def log(m)
  puts m
end

# Helper which makes a telnet call but yield block as complete lines instead of sub string
# TODO: The complete output is stored in a variable which isn't very memory efficient
def call(cmd, match, on = @sb)
  lines = ''
  on.cmd( "String" => cmd, "Match" => match ) { |c| lines += c }
  if block_given?
    lines.split(/\n/).each { |l| 
      yield l unless l =~ /^$/
    }
  end
end

def get_power_state
  ans = ''
  call("GetPowerState", /GetPowerState: (standby|on)\n/) { |l|
    log(l)
    l =~ /(standby|on)/
    ans = $& unless $&.nil?  
  }
  return ans
end

def turn_on
  call("SetPowerState on yes", /SetPowerState: (OK|ParameterError)\n/)
end

def turn_off
  call("SetPowerState standby yes", /SetPowerState: (OK|ParameterError)\n/)
end

def get_active_server
  ans = nil
  conn = false
  # This sometimes return "GenericError" (i.e. no connected server) even if there is one
  call("GetConnectedServer", /GetConnectedServer: (OK|GenericError)\n/) { |l|
    log(l)
    conn = true if l =~ /OK/
  }
  if conn
    call("GetActiveServerInfo", /GetActiveServerInfo: (OK|ErrorDisconnected)\n/) { |l|
      log(l)
      if l =~ /GetActiveServerInfo: Name: /
        ans = $'.strip if $'
      end
    }
  end
  return ans
end

def get_servers
  ans = []
  call("ListServers", /ListServers: ListResultEnd\n/) { |l|
    unless l =~ /ListResultSize|ListResultEnd/ 
      log(l)
      l =~ /ListServers: /
      ans << $'.strip if $'
    end
  }
  return ans
end

def connect_server(n)
  ans = false
  errors = "ParameterError|ConnectionFailedAlreadyConnected|ResourceAllocationError"
  errors += "|ConnectionFailedNoContact|ConnectionFailedUnknown|GenericError"
  call("ServerConnect #{n.to_s}", /ServerConnect: (TransactionComplete|#{errors})\n/) { |l|
    log(l)
    ans = true if l =~ /ServerConnect: (Connected|ConnectionFailedAlreadyConnected)/
  }
  return ans
end

def disconnect_server
  ans = false
  errors = "ErrorDisconnected|ResourceAllocationError|GenericError"
  call("ServerDisconnect", /ServerDisconnect: (TransactionComplete|#{errors})\n/) { |l|
    log(l)
    ans = true if l =~ /ServerDisconnect: Disconnected/
  }    
  return ans
end

def get_container_path
  ans = nil
  call("GetCurrentContainerPath", /GetCurrentContainerPath: .+\n/) { |l|
    log(l)
    l =~ /GetCurrentContainerPath: /
  }
  ans = $'.strip if $'
  return ans
end

def container_list
  ans = []
  errors = "ErrorDisconnected|GenericError"
  call("ListContainerContents", /ListContainerContents: (TransactionComplete|#{errors})\n/) { |l|
    log(l)
    unless l =~ /ListResultSize|ListResultEnd|TransactionInitiated|TransactionComplete/ 
      l =~ /ListContainerContents: /
      ans << $'.strip if $'
    end
  }
  return ans
end

def container_exit
  ans = false
  call("ContainerExit", /ContainerExit: (OK|ParameterError)\n/) { |l|
    log(l)
    ans = true if l =~ /OK/
  }
  ans
end

def container_enter(n)
  ans = false
  call("ContainerEnter #{n.to_s}", /ContainerEnter: (OK|ParameterError)\n/) { |l|
    log(l)
    ans = true if l =~ /OK/
  }
  return ans
end

def container_play(n)
  call("QueueAndPlayOne #{n.to_s}", /QueueAndPlayOne: .+\n/)
end

def get_transport_state
  ans = ''
  call("GetTransportState", /GetTransportState: .+\n/) { |l|
    log(l)
    l =~ /GetTransportState: /
    ans = $' unless $&.nil?  
  }
  return ans
end

# Shell utilities

def text_init
  call("sketch -c encoding utf8", /SoundBridge> /, @sh)
end

def text_clear
  call("sketch -c clear", /SoundBridge> /, @sh)
end

def text(x, y, message)
  call("sketch -c text #{x} #{y} \"#{message}\"", /SoundBridge> /, @sh)
end

def text_quit
  call("sketch -c quit", /SoundBridge> /, @sh)
end

# Main session

if ARGV[0] && ARGV[0] =~ /auto/
  t = Time.now
  hour = (t.min < 30) ? t.hour : t.hour + 1
  @item = ITEMS[hour%12]
elsif ARGV[0] && ARGV[0].to_i >= 0 && ARGV[0].to_i <= 12
  @item = ITEMS[ARGV[0].to_i]
else
  puts "Play bird sound with index 0 to 12 OR select automatically based on closest hour"
  puts "Usage: ./birdclock.rb auto"
  puts "       ./birdclock.rb n"
  exit 1
end

t = Time.now
t0 = "#{@item}"
t1 = DAYS[t.strftime("%A").to_i] + t.strftime(" %H:%M")
puts t0
puts t1

connect

unless get_power_state =~ /on/
  turn_on
  sleep 3
else
  if get_transport_state =~ /Play/
    puts "Don't interrupt ongoing playback"; quit_session(0, true)
  end
end

active_server = get_active_server
unless active_server && active_server == SERVER
  disconnect_server
  sleep 2
  servers = get_servers
  n = servers.index(SERVER)
  if n
    connect_server(n)
    sleep 2
  else
    puts "Failed to connect to server #{SERVER}"; quit_session 1
  end
end

path = get_container_path
until (path == "/")
  container_exit
  path = get_container_path
end

PATH.each { |c|
  items = container_list
  n = items.index(c)
  if n
    container_enter(n)
  else
    puts "#{c} container not found"; quit_session 1
  end
}

text_init
text_clear
text((39-t0.size)/2, 0, t0)
text((39-t1.size)/2, 1, t1)
sleep 10
text_quit

items = container_list
n = items.index(@item)
if n
  container_play(n)
else
  puts "#{@item} item not found"; quit_session 1
end

while get_transport_state =~ /Play/ do
  sleep 5
end

quit_session

References