Sunday, May 10, 2020

Weather data from openweathermap.org

Sharing my python code to pull data from OpenWeatherMap.  As of this writing, API access is free for some data.  Below is a sample to get the data and display it.

Here is a sample of the output produced by the script below:

07:14 PM
Feels Like: 35.7°F
Outside Temp: 48.6°F
Weather Look: Rain
Weather Desc: moderate rain
Humidity: 87%
Wind Speed: 20.80 mph
Wind Degree: 270°

In the code below, the city code: 5391959 is for San Francisco, CA.  You can modify your city search as needed.  The API documentation will show you how.

In the script, I'm using configparser to store critical data like the API Key.  An example of the file's content:

[openweathermap]
apiKey=439d4b804bc8187953eb36d2a8c26a02

The source for openweather.py is seen below and is available for download too:

<--- Begin Code --->

#!/usr/bin/python3

import sys, os, time, json, configparser, datetime, signal
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

def signal_handler (signal, frame):
    sys.exit(0)

def openweather():

    # Read Config parameters
    config = configparser.ConfigParser()
    config.read('/home/config.ini')
    apiKey = config['openweathermap']['apiKey']
    
    base_url = "https://api.openweathermap.org/data/2.5/weather?"
    complete_url = base_url + 'id=5391959&appid=' + apiKey
    response = requests.get(complete_url)
    if response.status_code == 200:
        return response.json()
    else:
        print(response.text)
        sys.exit(2)

def main():

    response = openweather()
    outtemp = float((response['main']['temp']-273.15)*9/5+32)
    feeltemp = float((response['main']['feels_like']-273.15)*9/5+32)
    outtemp = str("{0:.1f}".format(outtemp))
    feeltemp = str("{0:.1f}".format(feeltemp))
    weather_main = response['weather'][0]['main']
    weather_desc = response['weather'][0]['description']
    humidity = response['main']['humidity']
    wind_speed = float(response['wind']['speed'])
    wind_mph = format(wind_speed*2.2369363, '.2f')
    wind_deg = response['wind']['deg']
    currenttime = datetime.datetime.now()
    print(currenttime.strftime("%I:%M %p"))
    print("Feels Like: "+str(feeltemp)+u'\u00b0' + "F")
    print("Outside Temp: "+str(outtemp)+u'\u00b0' + "F")
    print("Weather Look: "+str(weather_main))
    print("Weather Desc: "+str(weather_desc))
    print("Humidity: "+str(humidity)+"%")
    print("Wind Speed: "+str(wind_mph)+" mph")
    print("Wind Degree: "+str(wind_deg)+u'\u00b0')

if __name__ == "__main__":
    signal.signal(signal.SIGINT, signal_handler)
    while True:
        main()
        time.sleep(30)

<--- End Code --->

Saturday, February 29, 2020

Clock with Weather - Pi Zero, ILI9341 & DS18B20

Clock and Temperature display


Using an ILI9341 SPI display with a DS18B20 Temperature sensor connected to a Raspberry PI Zero.

I did a search on Youtube and found a starting point.  The script I found had me running in just a few minutes.  I made some modifications and posted that below.  Hopefully this helps someone.

I'm using a SunFounder DS18B20 to get the temperature in the room.  I only want an accurate time source with the temperature.  Using a Pi Zero with NTP and the temperature sensor is all I need.  To display the data, I found a 2.2 inch ILI9341.  I hope to get this into a case more suitable for a desktop.

To get the DS18B20 connected, I followed the instructions from the vendor:  https://www.sunfounder.com/learn/sensor-kit-v2-0-for-raspberry-pi-b-plus/lesson-26-ds18b20-temperature-sensor-sensor-kit-v2-0-for-b-plus.html

To get the display connected and setup the modules, I did a couple of searches on the web and used this one: https://pi0cket.com/ili9341-raspberry-pi-guide/

My abbreviated config for the display is at the bottom of this post.  I was running Stretch Lite when I originally wrote this article.   For Buster Lite on a Pi display, I have those steps abbreviated at the bottom of this post.


Clock based on first script below

The three connections on the left are the temperature sensor.  From the grey wire to the right are the 9 connectors for the display

The other side of the Pi Zero to show those connections

The ILI9341 connections

SunFounder DS18B20

Raspberry PI Pinout

<---- ILI9341 Pin to Raspberry PI Zero Pin ---->

SDO/MISO ---- 21 (GPIO9)

LED      ---- 12 (GPIO18)

SCK      ---- 23 (GPIO11)

SDI/MOSI ---- 19 (GPIO10)

DC/RS    ---- 18 (GPIO24)

RESET    ---- 22 (GPIO25)

CS       ---- 24 (GPIO8)

GND      ---- 20 (GND)

VCC      ---- 17 (3v3)


<---- Sunfounder DS18B20 Sensor Pin to Raspberry PI Zero Pin ---->

SIG (1) ---- 7 (GPIO4)

VCC (2) ---- 2 (5v)

GND (3) ---- 6 (GND)


<---- Begin Code Section for myclock.py ---->

#!/usr/bin/python
#----------------------------------------------------------------
#       Note:
#               ds18b20's data pin must be connected to pin7.
#               replace the 28-XXXXXXXXX as yours.
#----------------------------------------------------------------

import pygame, sys, os, time, datetime, signal
from pygame.locals import *
os.environ["SDL_FBDEV"] = "/dev/fb1"

## Globals

pygame.init()

## Set up the screen

display_width = 320
display_height = 240
#display_width = 800
#display_height = 480

DISPLAYSURF = pygame.display.set_mode((display_width, display_height), 0, 16)
pygame.mouse.set_visible(0)
pygame.display.set_caption('Room Temp')

# set up the colors
BLACK = (  0,   0,   0)
WHITE = (255, 255, 255)
RED   = (255,   0,   0)
GREEN = (  0, 255,   0)
COBALTGREEN = ( 61, 145,  64)
BLUE  = (  0,   0, 255)
CYAN  = (  0, 255, 255)
YELLOW  = (255, 255,  0)
BANANA = (227,207,87)
GOLD1 = (255,215,0)
EMERALDGREEN = (0, 201, 87)
ALICEBLUE = (240,248,255)

ds18b20 = ''

def setup():
        global ds18b20
        for i in os.listdir('/sys/bus/w1/devices'):
                if i != 'w1_bus_master1':
                        ds18b20 = i

def readTemp():
#       location = '/sys/bus/w1/devices/28-00000a423922/w1_slave'
        location = '/sys/bus/w1/devices/' + ds18b20 + '/w1_slave'
        tfile = open(location)
        text = tfile.read()
        tfile.close()
        secondline = text.split("\n")[1]
        temperaturedata = secondline.split(" ")[9]
        temperature = float(temperaturedata[2:])
        temperature = temperature / 1000
        return temperature

def signal_handler (signal, frame):
        pygame.quit()
        sys.exit(0)

def DrawLine(color, startX,startY,stopX,stopY):
        pygame.draw.line(DISPLAYSURF, color, [startX,startY], [stopX,stopY], 1)

## Start
signal.signal(signal.SIGINT, signal_handler)
setup()

## Main loop

while True:

        currenttime = datetime.datetime.now()

        if readTemp() != None:
                currenttemp = readTemp()
                temp_c = str("{0:.1f}".format(currenttemp))
                temp_f = str("{0:.1f}".format((currenttemp*9/5)+32))

## Draw the title

        black_square_that_is_the_size_of_the_screen = pygame.Surface(DISPLAYSURF.get_size())
        black_square_that_is_the_size_of_the_screen.fill((0, 0, 0))
        DISPLAYSURF.blit(black_square_that_is_the_size_of_the_screen, (0, 0))

        font = pygame.font.Font(None, 30)
        text = font.render("Room Temp", 1, EMERALDGREEN)
        textpos = text.get_rect(center=(display_width*.5,int(round(.08*display_height))))
        DISPLAYSURF.blit(text, textpos)

## Draw temperatures

        font = pygame.font.Font(None, 70)
        text = font.render(temp_f, 1, GOLD1)
        textpos = text.get_rect(center=(display_width*.25,int(round(.30*display_height))))
        DISPLAYSURF.blit(text, textpos)

        font = pygame.font.Font(None, 20)
        textF = font.render(u'\u00b0' + "F", 1, GOLD1)
        textposF = textpos[0] + textpos[2], textpos[1] + 10
        DISPLAYSURF.blit(textF, textposF)

        font = pygame.font.Font(None, 70)
        text = font.render(temp_c, 1, GOLD1)
        textpos = text.get_rect(center=(display_width*.75,int(round(.30*display_height))))
        DISPLAYSURF.blit(text, textpos)

        font = pygame.font.Font(None, 20)
        textF = font.render(u'\u00b0' + "C", 1, GOLD1)
        textposF = textpos[0] + textpos[2], textpos[1] + 10
        DISPLAYSURF.blit(textF, textposF)

## Draw date

        font = pygame.font.Font(None, 40)
        text = font.render(currenttime.strftime("%A, %b %d"), 1, ALICEBLUE)
        textpos = text.get_rect(center=(display_width/2,int(round(.58*display_height))))
        DISPLAYSURF.blit(text, textpos)

## Draw time

        font = pygame.font.Font(None, 85)
        text = font.render(currenttime.strftime("%I:%M %p"), 1, ALICEBLUE)
        textpos = text.get_rect(center=(display_width/2,int(round(.79*display_height))))
        DISPLAYSURF.blit(text, textpos)

## Draw Lines

        DrawLine(EMERALDGREEN, 5, int(round(.16*display_height)), display_width-5, int(round(.16*display_height)))
        DrawLine(EMERALDGREEN, 5, int(round(.45*display_height)), display_width-5, int(round(.45*display_height)))
        DrawLine(EMERALDGREEN, display_width*.5, int(round(.16*display_height)), display_width*.5, int(round(.45*display_height)))

## Update the LCD

        pygame.display.update()

## Sleep time!

        time.sleep(15)

<---- End Code Section ---->


Abbreviated display configuration:

Assuming you have updated your install (apt update and apt upgrade) and have the DS18B20 connected and working, here are the steps I used based on the Pi0cket blog mentioned above.  These steps will get the display running your script automatically on a reboot:

1. I use python3 so make sure that is ready:
sudo rm /usr/bin/python
sudo ln -s /usr/bin/python3 /usr/bin/python
sudo apt install python3-pip
sudo pip3 install pygame==1.9.6

2. SDL 1.2 gets installed using the following:

sudo apt-get install libsdl1.2-dev

sudo apt-get install libsdl-image1.2-dev

sudo apt-get install libsdl-ttf2.0-dev

 
one line to get all 3:
sudo apt-get install -y libsdl1.2-dev libsdl-image1.2-dev libsdl-ttf2.0-dev

3. Execute:  sudo raspi-config

-Enable SPI (in raspi-config -> Interfacing Options -> P4 SPI -> Yes)
-Disable Overscan (in raspi-config -> Advanced Options -> A2 Overscan -> No) 
-Exit raspi-config


4. Execute:  sudo nano /etc/modules
at the bottom of file add:

spi-bcm2835
fbtft_device  

my file looks like this:

# /etc/modules: kernel modules to load at boot time.
#
# This file contains the names of kernel modules that should be loaded
# at boot time, one per line. Lines beginning with "#" are ignored.

spi-bcm2835
fbtft_device

5. Execute:  sudo nano /etc/modprobe.d/fbtft.conf

options fbtft_device name=fb_ili9341 gpios=reset:25,dc:24,led:18 speed=16000000 bgr=1 rotate=90 custom=1

my file has only one line and looks like this:
options fbtft_device name=fb_ili9341 gpios=reset:25,dc:24,led:18 speed=16000000 bgr=1 rotate=90 custom=1

 6.  Make the ILI9341 display show your content:

con2fbmap 1 1

7.  If the display shows up, reboot.

sudo reboot

8. I added the python script to run my display script by modifying rc.local
Execute:  sudo nano /etc/rc.local
add:  /home/pi/bin/myclock.ph &

my rc.local looks like this:

#!/bin/sh -e
#
# rc.local
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.


# Print the IP address
_IP=$(hostname -I) || true
if [ "$_IP" ]; then
  printf "My IP address is %s\n" "$_IP"
fi

sudo python /home/pi/myclock.py &

exit 0

9. If you're feeling comfortable, reboot your pi.
sudo reboot

You should see the results of your python script after the reboot is completed.

Utilize OpenWeatherMap data

I cleaned up some of my earlier script above.  I also added an API call to get some additional weather information using openweathermap.org data.  You'll need to register with openweathermap.  The basic weather information is free to access.  I wrote a separate blog that discusses openweathermap a little more.  If you only need that information, it's here.   Below is the python script with the additional data.

Shown with the added weather data

#!/usr/bin/python
#----------------------------------------------------------------
#       Note:
#               ds18b20's data pin must be connected to pin7.
#               replace the 28-XXXXXXXXX as yours.
#----------------------------------------------------------------

import pygame, sys, os, time, datetime, signal, requests
from pygame.locals import *
os.environ["SDL_FBDEV"] = "/dev/fb1"

## Globals

pygame.init()

## Set up the screen

display_width = 320
display_height = 240
display_center = int(display_width*.5)
display_rcenter = int(display_width*.75)
display_lcenter = int(display_width*.25)

DISPLAYSURF = pygame.display.set_mode((display_width, display_height), 0, 16)
pygame.mouse.set_visible(0)
pygame.display.set_caption('Room Temp')

# set up the colors
BLACK = (  0,   0,   0)
WHITE = (255, 255, 255)
RED   = (255,   0,   0)
GREEN = (  0, 255,   0)
COBALTGREEN = ( 61, 145,  64)
BLUE  = (  0,   0, 255)
CYAN  = (  0, 255, 255)
YELLOW  = (255, 255,  0)
BANANA = (227,207,87)
GOLD1 = (255,215,0)
EMERALDGREEN = (0, 201, 87)
ALICEBLUE = (240,248,255)
ROSYBROWN1 = (255,193,193)

ds18b20 = ''

def setup():
        global ds18b20
        for i in os.listdir('/sys/bus/w1/devices'):
                if i != 'w1_bus_master1':
                        ds18b20 = i

def readTemp():
#       location = '/sys/bus/w1/devices/28-00000a423922/w1_slave'
        location = '/sys/bus/w1/devices/' + ds18b20 + '/w1_slave'
        tfile = open(location)
        text = tfile.read()
        tfile.close()
        secondline = text.split("\n")[1]
        temperaturedata = secondline.split(" ")[9]
        temperature = float(temperaturedata[2:])
        temperature = temperature / 1000
        return temperature

def signal_handler (signal, frame):
        pygame.quit()
        sys.exit(0)

def DrawLine(color, startX,startY,stopX,stopY):
        pygame.draw.line(DISPLAYSURF, color, [startX,startY], [stopX,stopY], 1)

def openweather():
        api_key = "get your api key at openweathermap.org/api"
        base_url = "https://api.openweathermap.org/data/2.5/weather?"
        complete_url = base_url + 'id=5391959&appid=' + api_key
        response = requests.get(complete_url)
        if response.status_code != 200:
                exit
        return response.json()


## Start
signal.signal(signal.SIGINT, signal_handler)
setup()

## Main loop

while True:

        currenttemp = readTemp()
        temp_c = str("{0:.1f}".format(currenttemp))
        temp_f = str("{0:.1f}".format((currenttemp*9/5)+32))

        response = openweather()
        outtemp = float((response['main']['temp']-273.15)*9/5+32)
        feeltemp = float((response['main']['feels_like']-273.15)*9/5+32)
        outtemp = str("{0:.1f}".format(outtemp))
        feeltemp = str("{0:.1f}".format(feeltemp))
        weather_desc = response['weather'][0]['description']

## Draw the title

        blank_screen = pygame.Surface(DISPLAYSURF.get_size())
        blank_screen.fill((0, 0, 0))
        DISPLAYSURF.blit(blank_screen, (0, 0))

        font = pygame.font.Font(None, 25)
        text = font.render("Room", 1, EMERALDGREEN)
        textpos = text.get_rect(center=(display_lcenter,16))
        DISPLAYSURF.blit(text, textpos)

        font = pygame.font.Font(None, 25)
        text = font.render("Outside/Feel", 1, EMERALDGREEN)
        textpos = text.get_rect(center=(display_rcenter,16))
        DISPLAYSURF.blit(text, textpos)

## Draw Lines

        DrawLine(COBALTGREEN, 5, 28, display_width-5, 28)
        DrawLine(COBALTGREEN, 5, 114, display_width-5, 114)
        DrawLine(COBALTGREEN, display_center, 28, display_center, 114)

## Draw temperatures

        font = pygame.font.Font(None, 70)
        text = font.render(temp_f, 1, GOLD1)
        textpos = text.get_rect(center=(display_lcenter,56))
        DISPLAYSURF.blit(text, textpos)

        font = pygame.font.Font(None, 20)
        textF = font.render(u'\u00b0' + "F", 1, GOLD1)
        textposF = textpos[0] + textpos[2], textpos[1] + 6
        DISPLAYSURF.blit(textF, textposF)

        font = pygame.font.Font(None, 50)
        text = font.render(temp_c, 1, GOLD1)
        textpos = text.get_rect(center=(display_lcenter,96))
        DISPLAYSURF.blit(text, textpos)

        font = pygame.font.Font(None, 20)
        textF = font.render(u'\u00b0' + "C", 1, GOLD1)
        textposF = textpos[0] + textpos[2], textpos[1] + 5
        DISPLAYSURF.blit(textF, textposF)

        font = pygame.font.Font(None, 70)
        text = font.render(outtemp, 1, GOLD1)
        textpos = text.get_rect(center=(display_rcenter,56))
        DISPLAYSURF.blit(text, textpos)

        font = pygame.font.Font(None, 20)
        textF = font.render(u'\u00b0' + "F", 1, GOLD1)
        textposF = textpos[0] + textpos[2], textpos[1] + 6
        DISPLAYSURF.blit(textF, textposF)

        font = pygame.font.Font(None, 50)
        text = font.render(feeltemp, 1, GOLD1)
        textpos = text.get_rect(center=(display_rcenter,96))
        DISPLAYSURF.blit(text, textpos)

        font = pygame.font.Font(None, 20)
        textF = font.render(u'\u00b0' + "F", 1, GOLD1)
        textposF = textpos[0] + textpos[2], textpos[1] + 5
        DISPLAYSURF.blit(textF, textposF)

        font = pygame.font.Font(None, 20)
        text = font.render(weather_desc, 1, ROSYBROWN1)
        textpos = text.get_rect(center=(display_rcenter,126))
        DISPLAYSURF.blit(text, textpos)

## Draw time

        currenttime = datetime.datetime.now()

        font = pygame.font.Font(None, 85)
        text = font.render(currenttime.strftime("%I:%M %p"), 1, WHITE)
        textpos = text.get_rect(center=(display_center,180))
        DISPLAYSURF.blit(text, textpos)

## Draw date

        font = pygame.font.Font(None, 40)
        text = font.render(currenttime.strftime("%A, %b %d"), 1, ALICEBLUE)
        textpos = text.get_rect(center=(display_center,222))
        DISPLAYSURF.blit(text, textpos)

## Update the LCD

        pygame.display.update()

## Sleep time!

        time.sleep(15) 
 
Abbreviated Buster Steps:
buster lite with pi0 and ili9341 
1. make sure you perform rpi-update to update to the latest firmware:
sudo rpi-update 
2. sudo vi /boot/config.txt
- add to the end of the file: 
dtoverlay=fbtft,ili9341,dc_pin=24,reset_pin=25
dtparam=speed=16000000
dtparam=custom=1
dtparam=bgr=1
dtparam=led_pin=18
dtparam=rotate=90

3. I use python3 so make sure that is ready:
sudo rm /usr/bin/python
sudo ln -s /usr/bin/python3 /usr/bin/python
sudo apt install python3-pip
sudo pip3 install pygame==1.9.6

4. SDL 1.2 gets installed using the following:
sudo apt-get install libsdl1.2-dev

sudo apt-get install libsdl-image1.2-dev

sudo apt-get install libsdl-ttf2.0-dev


If you read the earlier instructions for the Stretch version, you don't need the module setup that we did.  You should be able to reboot the PI and see your display.

 

buster-lite with pi3b+ and 7 inch raspberry pi display

sudo vi /boot/config.txt
- add to the end of the file:
lcd_rotate=2

reboot
sudo raspi-config
Under System Options, set the following for your needs:
S1 Wireless LAN
S4 Hostname
S5 Boot / Auto Login

Under Display Options
D2 Underscan -> Yes to overscan

Interface Options
P2 SSH enable Yes
P7 1-Wire enable Yes

Configure Localization Options for your environment
L1 Locale
L2 Timezone
L3 Keyboard

Finish and reboot
Change the pi user password
passwd
Update and Install additional components:
sudo apt update
sudo apt -y upgrade
sudo rm /usr/bin/python
sudo ln -s /usr/bin/python3 /usr/bin/python
sudo apt install -y python3-pip
sudo apt install -y vim
sudo pip3 install pygame
sudo apt install -y libsdl2-2.0
sudo apt install -y python3-sdl2

start the clock 

Thursday, January 14, 2016

Airprint with OpenSUSE in my house

Two HP printers:
  • HP Color LaserJet 2600n
  • HP LaserJet CP1525nw
Quick todo List
  • OpenSUSE 13.2
  • CUPS
  • HPLIP
  • Configure CUPS
  • Run hp-setup (looks for a graphical display)
  • Avahi mime files generate
I'm running a VMware ESXi lab so it's an easy decision to install a Linux server that can act as the print server.  Installing OpenSUSE appears to automatically include CUPS during the basic deployment.  I won't go through the OS install here.  I'm going to assume the server is up and running for now and you are familiar with installing additional OpenSUSE applications in YaST.

In YaST, install HPLIP to provide the necessary printer drivers and tools.  To configure CUPS, I make the following changes in /etc/cups/cupsd.conf (add or modify these in the file - the following is just a subset of the cupsd.conf file):

# ServerAlias will prevent host name errors when a client makes a request
ServerAlias *

# By default, localhost is configured for Listen.  I want to manage from anywhere in my network.
Listen *:631

#My server has a static address of 192.168.1.11/24. 
# Restrict access to the server...
<Location />
  Order allow,deny
  Allow 127.0.0.2
  Allow 192.168.1.0/24
</Location>

# Restrict access to the admin pages...
<Location /admin>
  Order allow,deny
  Allow 192.168.1.0/24
</Location>

# Restrict access to configuration files...
<Location /admin/conf>
  AuthType Default
  Require user @SYSTEM
  Order allow,deny
  Allow 192.168.1.0/24
</Location>

Save the file and restart CUPS with with the command service cups restart.

I use the CUPS web portal to configure the printer (http://192.168.1.11:631).  HPLIP provides the correct drivers.  For the CP1525nw I'm using the driver HP LaserJet CP1520 Series Postscript.  For the 2600n, I use HP Color LaserJet 2600n hpijs.  You'll see that a proprietary driver is required.  We'll get to that in a moment.  I completed the add process for both printers.

For the proprietary driver, on the server as root, run the command hp-setup.  Select the 2600n in the list of printers and accept the defaults.  Once that was completed, I restarted CUPS from the terminal.  From the CUPS web portal, I printed a test page to both printers.  All should be successful at this point.

We're ready to make the printers Airprint-able.

Create the following two files:

/usr/share/cups/mime/airprint.convs with the content:
image/urf application/pdf 100 imagetoraster

/usr/share/cups/mime/airprint.types with the content:
image/urf urf string(0,UNIRAST<00>)

On your server as root, download and execute airprint-generate.py using the following:
wget https://github.com/tjfontaine/airprint-generate/archive/master.zip
unzip master.zip
cd airprint-generate-master
./airprint-generate.py

When I executed the script, I ended up with two files:
AirPrint-HP_Color_LaserJet_2600n.service
AirPrint-HP_LaserJet_CP1525nw.service

If you view the files in VI, you'll see see they are not formatted.  They're XML files so it's easy to fix.  Open each file in VI and enter the substitute command without quotes:
":1,$s/></>^M</g"  (to get the ^M, you'll need to use the keystrokes ctrl-v ctrl-m) 

The substitution will use place a line feed after each '>'.  You should now see a better formatted file.  Save the file and exit vi.

Copy the two files to /etc/avahi/services/

Restart Avahi with service avahi-daemon restart

You should now see the printers on your devices.  For me, I use the AirPrint advertised printers on our iPads, iPhones and Macs.

Monday, June 9, 2014

Multiple FLAC to single CUE-WAV script

For a while I've thought about creating single files with corresponding cue files.  Instead of re-ripping my collection, the script below will reassemble the flac files in a directory to a single wav and then create the necessary cue file.  It's a simple script and only processes one directory at a time.  Later, I'll put the code into this to process an artist and all album directories.

#!/usr/bin/perl
use strict;
use warnings;
use Cwd;
use File::Glob;
use Sysadm::Install qw(tap);

# Read directory for all flac
# soxi -d *.flac to get the times
# shntool join -o flac *.flac
# note: shntool cue *.flac > joined.cue will create the correct cue sheet without TITLE info.
# Had to install Sysadm::Install (did it through CPAN)

sub System_Call {
        my @args = @_;
        system (@args);
        if ($? == -1) {
                print "\nFailed to execute: $!\n";
                exit 2;
        }
        elsif ($? & 127) {
                printf "\nChild died with signal %d, %s coredump\n",
                ($? & 127),  ($? & 128) ? 'with' : 'without';
                exit 2;
        }
        return $?;
}

sub Get_Artist_Album {
        return ( split m!/!, cwd() )[-2,-1];
}

sub Add_Times {
        my $NewTime;
        my $BeginTime = $_[0];
        my $TrackLen = $_[1];
        my $NewSec = 0;
        my $NewMin = 0;
        my $NewHr = 0;
        my ($bMM, $bSS) = ( split m!:!, $BeginTime )[-2,-1];
        my ($tMM, $tSS) = ( split m!:!, $TrackLen )[-2,-1];
        $NewSec = $bSS+$tSS;
        if ($NewSec > 59) {
                $bMM = $bMM + 1;
                $NewSec = $NewSec-60;
        }
        $NewMin = $bMM+$tMM;
#       if ($NewMin > 59) {
#               $NewMin = $NewMin-60;
#       }
        $NewTime = sprintf("%02d",$NewMin).":".sprintf("%02.2f",$NewSec);
        return $NewTime;
}

sub Create_CUE {
        my $WAV_FILE = $_[0] or die "I need the name of the WAV\n\n";;
        my $i = 1;
        my $Track = 1;
        my @files = sort <*.flac>;
        my $StartTime = "00:00:00";
        my ($Artist, $Album) = Get_Artist_Album;
        open (CUEFILE, '>CDImage.cue');
        print CUEFILE "PERFORMER \"".$Artist."\"\n";
        print CUEFILE "TITLE \"".$Album."\"\n";
        print CUEFILE "FILE \"$WAV_FILE\" WAVE\n";
        foreach my $file (@files) {
                $Track = sprintf("%02d", $i);
                my $Title = $file;
                $Title =~ s{\.[^.]+$}{}; #Remove the extension
                $Title =~ s(^\d+[\.\_\-\ ]+)(); #Remove the leading track number
                print CUEFILE "  TRACK ".$Track." AUDIO\n";
                print CUEFILE "    TITLE \"".$Title."\"\n";
                if ($i > 1) {
                        my ($NewMM, $NewSS) = ( split m!:!, $StartTime )[-2,-1];
                        my ($NewSecL,$NewSecR) = ( split m!\.!, $NewSS )[-2,-1];
                        $NewSecL = sprintf("%02d",$NewSecL);
                        $NewSecR = $NewSecR * 0.6;
                        $NewSecR = sprintf("%02d",$NewSecR);
                        my $NewSecF = $NewMM.":".$NewSecL.":".$NewSecR;
                        print CUEFILE "    INDEX 01 ".$NewSecF."\n";
                } else {
                        print CUEFILE "    INDEX 01 ".$StartTime."\n";
                }
                my ($TrackTime, $stderr, $rc) = tap "soxi", "-d", $file;
                $StartTime = Add_Times $StartTime, $TrackTime;
                $i++;
        }
        close (CUEFILE);
}

sub Create_Single_WAV {
        my @files = sort <*.flac>;
        my $rc = System_Call "shntool", "join", "-o wav", @files;
        print "Error Code: $rc \n";
        if ($rc == 0) {
                rename "joined.wav","CDImage.wav";
                return 0;
        }
        return $rc;
}

unless (-x "/usr/bin/shntool") {
        print "I need the program shntool to function\n";
        exit 1;
}

unless (-x "/usr/bin/soxi") {
        print "I need the program soxi to function\n";
        exit 1;
}

my $results = Create_Single_WAV();
if ($results == 0) {
        Create_CUE "CDImage.wav";
}

Wednesday, November 21, 2012

M.A.M.E CHD Convert v4 to v5

I think we can agree that converting the drive images from v4 to v5 is tedious.  Definitely something to be scripted.  Since it's easy to find a Windows' version of the chdman program (I'm using a version from .147u3), I decided to do the script in Windows with Perl.  If you don't have Perl for Windows, get it here.  Once Perl is installed, copy and save the script below.  The script will scan your mame/rom directory looking for sub-directories.  It will then change to each sub-dir looking for version 4 of the CHD.  If the CHD is version 4, it will check the v5 list.  If the CHD is version 4 and found in the v5 list, it will get converted to version 5.  The v5 list is based on 147u3.  I'll try to keep it up to date.  Hope this helps someone.

Things to note.  I've already ran a CRC check against each CHD and know they all verify successfully.  If you have a corrupt CHD and are not aware, this program will probably continue to run, the resulting CHD will not work (neither did the original CHD it tried to convert).  I didn't do a lot of error checking.  The qx process will capture all STDOUT.  The data is there if you wish to parse STDOUT looking for success or failures during the conversion process.  I've also commented out the deletion of the original file.  I should probably include moving the v4 file to a backup directory instead of leaving it in its original directory.

If you use the script, be sure to set the location of  your chdman.exe and the ROM directory within the script.  Not sure if anyone else has experienced chdman hangs but I've seen the application hang during the copy process.  I've read that the hang is due to a possible problem in the multi-processor code.  I haven't confirmed that but I did build a single vCPU guest in my virtual environment for this task.  No hangs yet.

#### Updated for .147u4 - 12/17/2012

#### Updated for .148 - 1/25/2012

#### Updated for .164 - 7/29/2015


<----  Begin script ---->

#!/usr/bin/perl -w
#
# Get info to determine version:
#    chdman info -i <input file name>
#    Will return "File Version: 4"
#
# Convert from version the latest version
#    chdman copy -i <input file name> -o <new file name>
#    remove the original file and rename the new file
#

our $CHDMAN="g:\\mame\\chdman.exe";
our $Temp_Name = "tempnew.chd";

our @CHD_V5 = qw(a51site4-2_01.chd
a51site4-2_0.chd
alien.chd
arctthnd.chd
uarctict.chd
area51t.chd
area51.chd
area51mx.chd
atronic.chd
gdl-0018.chd
bam2.chd
batlgear.chd
bg2_204j.chd
bg2_201j.chd
gdl-0023a.chd
beachhead2000_5-27-2003.chd
beachhead2002_5-27-2003.chd
beachhead2003desertwar_5-27-2003.chd
beachhead2000_9-16-2001.chd
bikiniko.chd
biofreak.chd
bldyr3b.chd
blitz.chd
blitz2k.chd
blitz99.chd
753jaa11.chd
853jaa11.chd
gcb07jca02.chd
gcb07jca01.chd
gcc01jca02.chd
gcc01jca01.chd
825jaa11.chd
847jaa11.chd
981jaa11.chd
a21jaa11.chd
b07jaa11.chd
993hdda01.chd
988jaa11.chd
858jaa11.chd
a05jaa11.chd
995jaa11.chd
c01jaa11.chd
c44jaa03.chd
985jaa01.chd
bntyhunt.chd
a45a02.chd
645c04.chd
rtimes.chd
telly.chd
calchase.chd
calspeed.chd
calspeda.chd
carnevil.chd
carnevi1.chd
cartfury.chd
chaosheat.chd
chaosheatj.chd
gds-0001.chd
gdl-0014a.chd
cliffhgr.chd
cobra.chd
922d02.chd
922b02.chd
comebaby.chd
gdx-0002b.chd
420uaa04.chd
csplayh1.chd
csplayh5.chd
csplayh7.chd
cubeqst.chd
885jab01.chd
ep_pharo.chd
810uba02.chd
szz_cf.chd
fateulc.chd
jam1-dvd0.chd
firefox.chd
flipmaze.chd
fuudol.chd
gamecst2.chd
gamecstl.chd
99bottles.chd
gammagic.chd
gauntdl.chd
gauntd24.chd
gauntleg.chd
gauntl12.chd
gdvsgd.chd
gobyrc.chd
rcdego.chd
941b02.chd
gdx-0013.chd
sed1dvd0.chd
vr_xp_system_6-11-2002.chd
globalvr_xp_system.chd
hydro.chd
hyperath.chd
hyperv2_pqi_6-12-02.chd
hyperv2_pqi_9-30-01.chd
hyprdriv.chd
gdl-0010.chd
gds-0039b.chd
gds-0027.chd
gds-0026b.chd
gds-0033.chd
gds-0032c.chd
jdreddb.chd
jdreddc.chd
jn010108.chd
cap-jjk-3.chd
cap-jjm-1.chd
b41c02.chd
junai.chd
junai2.chd
gdl-0040.chd
kdeadeye.chd
kn1-b.chd
kinst.chd
kinst2.chd
cdp-00146.chd
kollon.chd
kollonc.chd
landhigh.chd
mace.chd
macea.chd
mach3.chd
mahjngoh.chd
a40jab02.chd
maxforce.chd
c09c04.chd
c09d04.chd
gdx-0017f.chd
mjmania.chd
a29b02.chd
a29a02.chd
b33a02.chd
b47jxb02.chd
mwskinsa.chd
mwskins104.chd
mwskins.chd
nagano98.chd
720jaa01.chd
nbanfl.chd
nbashowt.chd
npy1cd0b.chd
nfsug1_1-disc2.chd
nfsug1_1-disc1.chd
gds-0023e.chd
nightrai.chd
offrthnd.chd
gdx-0007.chd
orbatak.chd
otenamhf.chd
otenamih.chd
otenki.chd
gdx-0004a.chd
gdx-0014a.chd
a00jac02.chd
a00uad02.chd
a00kac02.chd
a00uac02.chd
a00eaa02.chd
b11a02.chd
pbball96.chd
pp201.chd
831jhdda01.chd
gq986jaa01.chd
gq986jaa02.chd
a04jaa02.chd
a04jaa01.chd
gqa16jaa01.chd
gqa16jaa02.chd
b00jab01.chd
b00jaa02.chd
gqb30jaa01.chd
gqb30jaa02.chd
c00jab.chd
gea02jaa02.chd
gea02jaa01.chd
977kaa02.chd
a11jaa01.chd
a11jaa02.chd
977jaa02.chd
gc977jaa02.chd
gc977jaa01.chd
ppp2nd.chd
primrag2.chd
psattack.chd
gdl-0024.chd
psyvaria.chd
psyvarrv.chd
gds-0031.chd
pwrshovl.chd
gq460a08.chd
ge557a09.chd
quakeat.chd
pqiidediskonmodule.chd
676a04.chd
gdl-0032a.chd
raizpin.chd
raycris.chd
cap-wzd-3.chd
roadburn.chd
rotr.chd
rrv1-a.chd
gca18jaa.chd
savquest.chd
gdx-0018a.chd
scp1cd0.chd
gdl-0030a.chd
sf2049se.chd
sf2049te.chd
sf2049.chd
sfrush.chd
sfrushrk.chd
gds-0016.chd
shanghss.chd
shanghaito.chd
gdl-0021.chd
shikigam.chd
sianniv.chd
simpbowl.chd
sc21-dvd0b.chd
sc21-dvd0d.chd
sc31001-na-dvd0-b.chd
soutenry.chd
sf010101.chd
speeddrv.chd
spuzbobj.chd
spuzbobl.chd
a13b02.chd
a13c02.chd
gdl-0005.chd
hm-in2.chd
db1.chd
gv027j1.chd
tk10100-1-na-dvd0-a.chd
tk9100-1-na-dvd0-a.chd
tef1dvd0.chd
te51-dvd0.chd
tenthdeg.chd
thenanpa.chd
a41b02.chd
a41a02.chd
a41c02.chd
tst1dvd0.chd
755jaa01.chd
756jab01.chd
tokyocop.chd
gdl-0036a.chd
gdl-0026.chd
tkk2-a.chd
a30b02.chd
a30c02.chd
turrett.chd
gdl-0035.chd
usagi.chd
usvsthem.chd
vaportrx.chd
vaportrp.chd
vcircle.chd
gds-0036f.chd
voyager.chd
wmn1.chd
gdx-0016a.chd
warfa.chd
wargods_08-15-1996.chd
wargods_10-09-1996.chd
wargods_12-11-1995.chd
c22d02.chd
c22a02.chd
c22c02.chd
weddingr.chd
wg3dh.chd
c18jaa03.chd
c27jaa03.chd
xiistag.chd
b4xb02.chd
yuyuhaku.chd
zga1dvd0.chd
zdx1dvd0.chd
zokuoten.chd
zooo.chd
b44jaa01.chd
706jaa02.chd
887kba02.chd
887aaa02.chd
887jaa02.chd
887kaa02.chd
a22jaa02.chd
a34jaa02.chd
a27jaa02.chd
b19jaa02.chd
b20jaa02.chd
894jaa02.chd
a38jaa02.chd
845aaa02.chd
845jba02.chd
845jab02.chd
810eaa02.chd
gdt-0008c.chd
b17jaa02.chd
gdl-0001.chd
gdl-0006.chd
a12jaa01.chd
a12jaa02.chd
gdl-0034.chd
gdl-0039.chd
gdl-0039a.chd
gdl-0028c.chd
gds-0023c.chd
623jaa02.chd
802jab02.chd
gdl-0017.chd
gdl-0002.chd
gds-0004.chd
gds-0005.chd
gds-0019.chd
gdx-0003a.chd
gds-0024a.chd
gds-0036a.chd
gds-0036d.chd
gdt-0002.chd
gdt-0015.chd
gdt-0013e.chd
gds-0011.chd
gds-0010.chd
gdl-0020.chd
);

sub ctrl_c_handler {
 print "\nCtrl C pressed \n";
 exit 2;
}

sub CHD_Info
{
 my $ROM_Name = $_[0] or exit 1;
 my $CHDVer = 0;
 print $ROM_Name,"\n";
 my $output = qx{$CHDMAN info -i $ROM_Name};
 foreach (split(/\n/,$output)) {
 if ($_ =~ /^File/) {
   $CHDVer = $_;
   chomp $CHDVer;
  }
 }

 my $junk;
 ($junk, $CHDVer) = split(':', $CHDVer);

# $CHDVer = int($CHDVer);

 return int $CHDVer;
}

sub CHD_Copy
{
 my $ROM_Name = $_[0] or exit 1;
 my $stdout = qx {$CHDMAN copy -i $ROM_Name -o $Temp_Name};
 my $Old_Name = $ROM_Name . ".old";
 $stdout = qx {ren $ROM_Name $Old_Name};
 $stdout = qx {ren $Temp_Name $ROM_Name};
# $stdout = qx {del $Old_Name};
}

unless (-f $CHDMAN) {
    print "We need a working chdmgr program.  Check the variable CHDMAN in the script\n";
    exit 1;
}
print "Lets go...\n";

$SIG {"INT"} = \&ctrl_c_handler; # "INT" indicates "Interrupt" signal.

my $Src_Dir = "g:\\mame\\roms";
my $Version;
chdir 'g:\\mame\\roms';

my @Src_Chds = qx {dir /AD /b /on};
foreach my $Src_Chd (@Src_Chds) {
 chomp $Src_Chd;
 chdir $Src_Chd;
 print "Directory: ", $Src_Chd, "\n";
 if (-e $Temp_Name) {
  unlink $Temp_Name;
 }
 my @CHDS = qx {dir *.chd /b};
 foreach my $CHD (@CHDS) {
  chomp $CHD;
  if (grep {$_ eq $CHD} @CHD_V5) {
    $Version = CHD_Info $CHD;
    if ($Version == 4) {
     print "\n", $CHD, " is version: ", $Version, " - Now Converting to v5\n";
     CHD_Copy $CHD;
    }
    }
 }
 chdir "..";
}

<----End Script---->

Monday, February 13, 2012

iPhone, MythTV, UPnP and miniDLNA

I have a MythTV backend running on Mythbuntu.  Great for recording TV shows, watching videos and listening to music from the MythTV Frontend client.  I want to be able to listen to my music from anywhere around the house.  I prefer FLAC over MP3 and wanted a method to stream music to my stereo or any docking device.  I have an iPhone, Touch and iPad; these should make nice clients given a decent application.  I don't like the UPnP service that MythTV provides.  After a little testing, I settled on miniDLNA on the server with the 8player app on my Apple devices.

MiniDLNA offers a search function for Artists or Albums.  Other UPnP servers did not have the same extended capability (of the ones I tried).  MiniDLNA also shows album art.  8player has a free version of the app to test drive.  I'm still waiting to get a DLNA or UPnP service that can stream a DVD ISO (converting to MP4 stinks).

Update Oct 20, 2012:  I've added a Micca EP600 G2 to my man cave.  This device allows me watch my DVD ISO files and play FLAC music.  It offers support for many formats.  I also have some DTS DVD-Audio that it plays without any issue.  The device offers HDMI connectivity.  Installation is a snap.  I'm using a Samba share on my Mythbuntu box to access the media with the Micca device.  I'm not happy with the inability to auto-display album artwork.  While the documentation reads as though the album artwork is displayed if found, I have yet to see the artwork unless I open it manually.  Hopefully, just an operator malfunction and I'll sort that out.  For now, I can live with that as the good outweighs the bad.

With the addition of the Micca device, I found that I don't use my Mythfrontend.  For me, the main purpose of the Myth services was playback of videos and music.  I never used the service to record television shows.  If you've tried Mythmusic 0.25, maybe you're as pissed about the changes as I am.  I hate it.  This made my decision easier than it might have been otherwise.  I'll stop my rant and move forward.  I've decided to get rid of the Myth service entirely.  With the removal of the Myth software, I will use my VMware ESXi server to host the services mentioned above.  I already have a CentOS 5 guest running as a virtual machine.  My plan is to move all the media data to the NAS device and create some mount points on the CentOS system for music and videos.  I'm waiting for some hardware that I bought on the 'net.  Once that arrives, It will take about a day to migrate the data from my physical Mythbuntu server to the CentOS VM (my music and video collection accounts for nearly 4TB of data).  I'll update this once I complete the physical to virtual migration.

Update Dec 7, 2012:  I moved my media server to a virtual machine.  Settled on openSuSE for the OS on the virtual guest.  The system has been running since I did the migration at the end of Oct.  Still running a samba share as well as miniDLNA.  All still working without any issues.  My iPhone, Touch and iPad still accessing the audio files with 8player and miniDLNA.  My Micca device is accessing the samba share grabbing the ISO images and FLAC audio.  I could not be happier with the performance and the ease of this environment.

Update June 8, 2014:  No real changes.  I updated to openSuSE 13.1 recently and added a second NAS.  I still like the layout and easy access to music and video material in any room of the house.  I did add an antenna booster to my WiFi network allowing me to listen to music while cutting grass or working in the yard.  One of my kids recently gave me their broken iPhone 4.  After a replacement case from Zeetron, the device is excellent for around-the-house work or walking on the treadmill.  I need a new hobby.

Update February 11, 2015:  Still using and happy with the Micca EP600 devices.  I put a television in the treadmill room and needed another media device.  This time, I wanted to try a different solution and took a chance on the Western Digital WD TV Live (model WDBGXT0000NBK).  It's only been a couple of weeks, but I believe the WD is more favorable.  It does everything the Micca does but also includes services like NetFlix.  For the treadmill room, the TV does not have app capabilities so the one device covers everything for me.  I purchased the device on Amazon as a refurbished model for $64.  The device handles Bluray and DVD ISO files along with nearly every possible format for those that rip the media down to something else.  I can also play my music (FLAC, MP3, etc) and audio images (CUE/Wav, CUE/Flac).  Artwork is also displayed while browsing and playing the audio as well.  The only thing I don't like about this model; the time is always off.  Not sure why.  The only options for time are zone and daylight savings.  No way to actually set the time or specify an NTP source.

Sunday, January 29, 2012

DVD to MP4 with HandBrake (vidconv.sh)

Using Handbrake in a bash script.

This script processes my ISO file(s) and converts them to MP4.  The MP4 can then be used via UPNP or DLNA on my network devices.  I rip my DVDs to ISO and store them on my media server.  This is a living script and may change.  It takes a while to process each ISO at the moment.  I just want good quality when I view the video on a widescreen television.  The options for Handbrake I'm using:

-m                    Chapter markers
--main-feature   Find and process the main title.  By default, Title 1 is processed.
-e x264             Video encoder
-q 20                 Video quality
-r 29.97             Video frame rate
-b 2000             Video bitrate
-B 192               Audio bitrate
-i /mnt                This is the mount point for the ISO
-o                      Output file name
2> /dev/null        I don't want to see everything displayed to the screen - sending std error to nowhere land.

The script requires you to run as root since we are running mount and umount.

<<< --- vidconv.sh --->>>
#!/bin/bash

function ConvertIt()
{
    NewName=${1%%\.*}
    EXT=mp4
    if [ -f "$NewName.$EXT" ]; then
        echo "A file by the name of $NewName.$EXT already exists"
        return 1
    fi
    echo Processing:  $NewName
    mount -o loop "$1" /mnt
    HandBrakeCLI -m --main-feature -e x264 -q 20 -r 29.97 -b 2000 -B 192 -i /mnt -o "$NewName.$EXT" 2> /dev/null
    echo
    umount /mnt
}

if [[ $EUID -ne 0 ]]; then
   echo "This script must be run as root" 1>&2
   exit 1
fi
if [ "$1" = "" ]; then
    echo "You need to tell me which ISO to convert"
    echo "Use -a to process all ISO files in the currentl directory"
    exit 1
fi

if [ "$1" = "-a" ]; then
        for files in *.iso
        do
                ConvertIt "$files"
        done
else
    if [ ! -f "$1" ]; then
        echo "The input file $1 does not exist."
        exit 1
    fi
    ConvertIt "$1"
fi

exit 0