Information wants to be free...

FLAC to MP3 Conversion

Here is my custom Python script to convert FLAC files to MP3 files.
In addition to the obligatory audio conversion, it also handles tag conversion. Because of this, the external eyeD3 module is required.

Take a look:

#!/usr/bin/python

import subprocess
import eyed3
import os
import os.path
import re
import struct
import getopt

temp_tags = r"/tmp/___TAGS___.utf8"
temp_cover = r"/tmp/___COVER___.jpg"

def apic_from_unicode_to_latin1(filename):
    data = open(filename, "rb").read()

    match = re.search('APIC(......)\x01image/jpeg\x00\x03\xff\xfe\x00\x00', data)
    if match:
        header = match.group(1)

        # Just inject FIX as picture description to keep same frame size!
        old = 'APIC' + header + '\x01image/jpeg\x00\x03\xff\xfe\x00\x00'
        new = 'APIC' + header + '\x00image/jpeg\x00\x03FIX\x00'
        data = data.replace(old, new)

        fh = open(filename, "wb")
        fh.write(data)
        fh.close()

class TagData(object):
    def __init__(self):
        self.title = None
        self.album = None
        self.track_num = None
        self.artist = None
        self.genre = None
        self.date = None
        self.album_artist = None

    def read_flac_tags(self, filename):
        subprocess.call(["metaflac", "--no-utf8-convert", "--export-tags-to=" + temp_tags, filename])
        fh = open(temp_tags, "rb")
        for line in fh:
            match = re.match(r'^TITLE=(.*)', line.decode('UTF-8'), re.IGNORECASE)
            if match:
                self.title = match.group(1)
            match = re.match(r'^ALBUM=(.*)', line.decode('UTF-8'), re.IGNORECASE)
            if match:
                self.album = match.group(1)
            match = re.match(r'^TRACKNUMBER=(\d*)', line.decode('UTF-8'), re.IGNORECASE)
            if match:
                self.track_num = int(match.group(1))
            match = re.match(r'^ARTIST=(.*)', line.decode('UTF-8'), re.IGNORECASE)
            if match:
                self.artist = match.group(1)
            match = re.match(r'^GENRE=(.*)', line.decode('UTF-8'), re.IGNORECASE)
            if match:
                self.genre = match.group(1)
            match = re.match(r'^DATE=(\d*)', line.decode('UTF-8'), re.IGNORECASE)
            if match:
                self.date = int(match.group(1))

        fh.close()

    def write_mp3_tags(self, filename):
        audiofile = eyed3.load(filename)
        audiofile.tag = eyed3.id3.Tag() # Create NEW!
        if self.title:
            audiofile.tag.title = self.title
        if self.album:
            audiofile.tag.album = self.album
        if self.track_num:
            audiofile.tag.track_num = self.track_num
        if self.artist:
            audiofile.tag.artist = self.artist
        if self.genre:
            audiofile.tag.genre = self.genre
        if self.date:
            audiofile.tag.recording_date = self.date # Works if version 2.3
        if self.album_artist:
            audiofile.tag.album_artist = self.album_artist
        audiofile.tag.save(filename, eyed3.id3.ID3_V2_3)

def print_usage(progname):
    print "Usage: %s " % (progname)
    print "-f <FLAC File>"
    print "-a [AlbumArtist]"
    print "-c [Cover Image Path]"
    print "-n [Album Name Override]"

if __name__ == "__main__":
    import sys

    tagdata = TagData()

    # NOTE: Need to filter out empty strings from sys.argv.
    # If not, the parser thinks parsing finishes early.
    try:
        opts, args = getopt.getopt(filter(None, sys.argv[1:]), "hf:a:c:n:", ["help", "flacfile=", "albumartist=", "cover=", "albumname="])
    except getopt.GetoptError as err:
        print str(err)
        print_usage(sys.argv[0])
        sys.exit(1)

    print "Opts:", opts
    print "Args:", args

    filename = None
    album_artist = None
    cover_override = None
    album_name_override = None

    for o, a in opts:
        if o in ("-h", "--help"):
            print_usage(sys.argv[0])
            sys.exit(1)
        elif o in ("-f", "--flacfile"):
            filename = a
            print "FLAC File:", filename
        elif o in ("-a", "--albumartist"):
            album_artist = a
            print "AlbumArtist:", album_artist
        elif o in ("-c", "--cover"):
            cover_override = a
            print "Cover Image Path:", cover_override
        elif o in ("-n", "--albumname"):
            album_name_override = a
            print "Album Name Override:", album_name_override

    if filename == None:
        print_usage(sys.argv[0])
        sys.exit(1)

    # Set optional AlbumArtist.
    if album_artist:
        tagdata.album_artist = unicode(album_artist)

    # Read old tags from FLAC.
    tagdata.read_flac_tags(filename)

    # Set optional album name override, AFTER reading default tags.
    if album_name_override:
        tagdata.album = unicode(album_name_override)

    # Decode FLAC, encode MP3.
    subprocess.call(["flac", "-d", filename, "-o", filename + ".wav"])
    subprocess.call(["lame", "-b", "320", "-h", filename + ".wav", filename + ".mp3"])

    # Write new tags to MP3.
    tagdata.write_mp3_tags(filename + ".mp3")

    # Get cover image from argument, or...
    if cover_override:
        cover_file = cover_override
    else:
        # ...attempt to extract cover image from FLAC file...
        subprocess.call(["metaflac", "--export-picture-to=" + temp_cover, filename])
        cover_file = temp_cover

    # ...then apply it.
    if os.path.isfile(cover_file):
        imagedata = open(cover_file, "rb").read()
        if imagedata.startswith("\xff\xd8\xff"):
            audiofile = eyed3.load(filename + ".mp3")
            audiofile.tag.images.set(3, imagedata, "image/jpeg")
            audiofile.tag.save()
        else:
            print "Warning: Image data is not JPEG."

    # iTunes APIC fix, encoding must be changed to latin1.
    apic_from_unicode_to_latin1(filename + ".mp3")

    # Remove ".flac" part of final MP3 file.
    if os.path.isfile(filename + ".mp3"):
        os.rename(filename + ".mp3", filename.replace(".flac", "") + ".mp3")

    # Remove temporary files.
    if os.path.isfile(filename + ".wav"):
        os.remove(filename + ".wav")
    if os.path.isfile(temp_tags):
        os.remove(temp_tags)
    if os.path.isfile(temp_cover):
        os.remove(temp_cover)
          


Topic: Scripts and Code, by Kjetil @ 03/12-2016, Article Link

Rename by Reference

Another specialized tool for a specialized task, this Python script renames files in a directory based on names from another directory. The pattern matching is only based on matching substrings. If a filename fits as a substring within another filename from the reference directory, then that reference filename is selected as a possible choice. Filename extensions are excluded automatically.

Take a look:

#!/usr/bin/python

import os
import glob

def rename_by_reference(target_dir, ref_dir):
    ref = dict()
    for f in glob.glob(os.path.join(ref_dir, "*")):
        if os.path.isdir(f):
            (name, ext) = (os.path.basename(f), "")
        else:
            (name, ext) = os.path.splitext(os.path.basename(f))
        ref[name] = ext

    for f in glob.glob(os.path.join(target_dir, "*")):
        if os.path.isdir(f):
            (name, ext) = (os.path.basename(f), "")
        else:
            (name, ext) = os.path.splitext(os.path.basename(f))
        for (ref_name, ref_ext) in ref.iteritems():
            if name == ref_name:
                continue # Equal, not interesting...
            if name in ref_name:
                print name + ext, "--->", ref_name + ext
                answer = raw_input("Rename? ").lower()
                if answer.startswith("q"):
                    return
                elif answer.startswith("y"):
                    src = f
                    dst = os.path.join(os.path.dirname(f), ref_name + ext)
                    os.rename(src, dst)
                    break

if __name__ == "__main__":
    import sys

    if len(sys.argv) < 3:
        print "Usage: %s <target dir> <reference dir>" % (sys.argv[0])
        sys.exit(1)

    rename_by_reference(sys.argv[1], sys.argv[2])
    sys.exit(0)
          


Topic: Scripts and Code, by Kjetil @ 09/10-2016, Article Link

AVR Alarm Clock

Here is an implementation of an alarm clock running on an Atmel AVR (ATMega8) microcontroller. It uses a 8-digit LCD display for output, a 3x4 button keypad for input and a buzzer to give an alarm.

Here is a circuit diagram of the whole setup:

AVR Alarm Clock Schematics

Get a higher resolution schematic here.

The software on the microcontroller is written entirely in AVR Assembly language. It features a menu to view/set the clock and view/set/enable the alarm and the buzzer. The clock itself is updated by a timer interrupt in the microcontroller, and is not very accurate when running on the internal clock unfortunately. Perhaps an external crystal would improve it, but I never got around to trying it due to lack of hardware.

I used the AVRA assembler and AVRDUDE utility against a Atmel STK500 board for development. Easy with this Makefile:

clock.hex clock.eep.hex: clock.asm
	avra clock.asm -o clock.hex

# Needs to be in the correct order, Flash first, then EEPROM second.
.PHONY: install
install: clock.hex clock.eep.hex
	avrdude -c stk500v2 -p m8 -P /dev/ttyS0 -U flash:w:clock.hex:i
	avrdude -c stk500v2 -p m8 -P /dev/ttyS0 -U eeprom:w:clock.eep.hex:i

.PHONY: clean
clean:
	rm clock.hex clock.eep.hex clock.obj clock.cof
          


Here is the code (clock.asm):

; *****************************************************************************
;  AVR ALARM CLOCK
; *****************************************************************************

.NOLIST
.INCLUDE "m8def.inc" ; ATMega8
.LIST

; =============================================================================
;  REGISTER DEFINITIONS
; =============================================================================

.DEF second = r0 ; Clock second.
.DEF minute = r1 ; Clock minute.
.DEF hour   = r2 ; Clock hour.

.DEF alarm_active = r3 ; Alarm active/enable flag.
.DEF alarm_minute = r4 ; Alarm minute.
.DEF alarm_hour   = r5 ; Alarm hour.

.DEF var1   = r16 ; Local use in routines.
.DEF var2   = r17 ; Local use in routines.
.DEF arg1   = r18 ; Argument (or return value) between routines.
.DEF arg2   = r19 ; Argument (or return value) between routines.
.DEF intvar = r20 ; Used only in interrupt routines.

; =============================================================================
;  VALUE LABELS
; =============================================================================

.EQU MENU_ENTRY_CLOCK_SHOW    = 0
.EQU MENU_ENTRY_CLOCK_SET     = 1
.EQU MENU_ENTRY_ALARM_SET     = 2
.EQU MENU_ENTRY_ALARM_ENABLE  = 3
.EQU MENU_ENTRY_BUZZER_STATUS = 4
.EQU MENU_ENTRY_END           = 5

.EQU CLOCK_SET_HOUR_MSD   = 0
.EQU CLOCK_SET_HOUR_LSD   = 1
.EQU CLOCK_SET_MINUTE_MSD = 2
.EQU CLOCK_SET_MINUTE_LSD = 3
.EQU CLOCK_SET_SECOND_MSD = 4
.EQU CLOCK_SET_SECOND_LSD = 5
.EQU CLOCK_SET_END        = 6

.EQU ALARM_SET_HOUR_MSD   = 0
.EQU ALARM_SET_HOUR_LSD   = 1
.EQU ALARM_SET_MINUTE_MSD = 2
.EQU ALARM_SET_MINUTE_LSD = 3
.EQU ALARM_SET_END        = 4

; =============================================================================
;  DATA DEFINITIONS
; =============================================================================

.DSEG
.ORG 0x0060
_keypad_key_previous: .BYTE 1

; =============================================================================
;  EEPROM DATA
; =============================================================================

.ESEG
.ORG 0x0000

clock_set_menu_text:
.db "ClockSet"
alarm_set_menu_text:
.db "AT:"
alarm_enable_menu_text:
.db "AE:"
buzzer_status_menu_text:
.db "Buzzer:"

; =============================================================================
;  RESET AND INTERRUPT VECTORS
; =============================================================================

.CSEG
.ORG 0x0000 ; Reset vector.
  rjmp main_init

.ORG 0x0006 ; Timer1 compare 'a' interrupt.
  rjmp timer1_interrupt

; =============================================================================
;  MAIN INITIALIZATION
; =============================================================================

.ORG 0x0010
main_init:
  ; Setup MCU control register.
  in var1, MCUCR
  ori var1, 0x80 ; Enable sleep in idle mode.
  out MCUCR, var1

  ; Define SRAM as stack. (In order to use subroutines.)
  ldi var1, HIGH(RAMEND)
  out SPH, var1
  ldi var1, LOW(RAMEND)
  out SPL, var1

  ; Clear registers.
  clr second
  clr minute
  clr hour
  clr alarm_active
  clr alarm_minute
  clr alarm_hour

  ; Clear data.
  ldi var1, 0
  sts _keypad_key_previous, var1

  ; Set parts of Port-B (Connected to LCD) as output.
  ldi var1, 0b00111111
  out DDRB, var1

  ; Set parts of Port D (Connected to Keypad and buzzer) as output.
  ldi var1, 0b10010101
  out DDRD, var1

  ; Enable compare match 'a' interrupt for Timer1.
  ldi var1, 0b00010000
  out TIMSK, var1

  ; Set compare value 'a' for Timer1.
  ; (Tuned to one second when using internal clock.)
  ldi var1, 0b00111100
  out OCR1AH, var1
  ldi var1, 0b10000000
  out OCR1AL, var1

  ; Clear Timer1 counter.
  clr var1
  out TCNT1H, var1
  out TCNT1L, var1

  ; Enable interrupts globally. 
  sei

  ; Start Timer1 in CTC mode and use internal clock with clk/64 prescaler.
  clr var1
  out TCCR1A, var1
  ldi var1, 0b00001011
  out TCCR1B, var1

  ; Initialize LCD.
  rcall lcd_init

  ; Start main program loop.
  rjmp menu_loop_init

; =============================================================================
;  MAIN MENU LOOP
; =============================================================================

menu_loop_init:
  clr var1 ; Holds "current entry". What to show and what action to take.
menu_loop:

  cpi var1, MENU_ENTRY_CLOCK_SHOW
  brne _menu_loop_display_clock_set

  ; Display clock.
  rcall lcd_first_position
  mov arg1, hour
  rcall split_msd_lsd
  rcall lcd_number
  ldi arg1, ':'
  rcall lcd_character
  mov arg1, minute
  rcall split_msd_lsd
  rcall lcd_number
  ldi arg1, ':'
  rcall lcd_character
  mov arg1, second
  rcall split_msd_lsd
  rcall lcd_number

; -----------------------------------------------------------------------------

_menu_loop_display_clock_set:
  cpi var1, MENU_ENTRY_CLOCK_SET
  brne _menu_loop_display_alarm_set

  ; Display "ClockSet".
  rcall lcd_first_position
  ldi arg1, LOW(clock_set_menu_text)
  ldi arg2, 8
  rcall lcd_eeprom_string
  rjmp _menu_loop_keypad_poll

; -----------------------------------------------------------------------------

_menu_loop_display_alarm_set:
  cpi var1, MENU_ENTRY_ALARM_SET
  brne _menu_loop_display_alarm_enable

  ; Display "AT:" and time of alarm.
  rcall lcd_first_position
  ldi arg1, LOW(alarm_set_menu_text)
  ldi arg2, 3
  rcall lcd_eeprom_string

  mov arg1, alarm_hour
  rcall split_msd_lsd
  rcall lcd_number
  ldi arg1, ':'
  rcall lcd_character
  mov arg1, alarm_minute
  rcall split_msd_lsd
  rcall lcd_number
  rjmp _menu_loop_keypad_poll

; -----------------------------------------------------------------------------

_menu_loop_display_alarm_enable:
  cpi var1, MENU_ENTRY_ALARM_ENABLE
  brne _menu_loop_display_buzzer_status

  ; Display "AE:" and 1 if active, or 0 if not active.
  rcall lcd_first_position
  ldi arg1, LOW(alarm_enable_menu_text)
  ldi arg2, 3
  rcall lcd_eeprom_string

  tst alarm_active
  breq _menu_loop_display_alarm_not_active

  ldi arg1, '1'
  rcall lcd_character
  ldi arg1, ' '
  rcall lcd_character
  rcall lcd_character
  rcall lcd_character
  rcall lcd_character
  rjmp _menu_loop_keypad_poll

_menu_loop_display_alarm_not_active:
  ldi arg1, '0'
  rcall lcd_character
  ldi arg1, ' '
  rcall lcd_character
  rcall lcd_character
  rcall lcd_character
  rcall lcd_character
  rjmp _menu_loop_keypad_poll

; -----------------------------------------------------------------------------

_menu_loop_display_buzzer_status:
  cpi var1, MENU_ENTRY_BUZZER_STATUS
  brne _menu_loop_keypad_poll

  ; Display "Buzzer:" and 1 if active, or 0 if not active.
  rcall lcd_first_position
  ldi arg1, LOW(buzzer_status_menu_text)
  ldi arg2, 7
  rcall lcd_eeprom_string

  rcall buzzer_is_active
  tst arg1
  breq _menu_loop_display_buzzer_status_not_active

  ldi arg1, '1'
  rcall lcd_character
  rjmp _menu_loop_keypad_poll

_menu_loop_display_buzzer_status_not_active:
  ldi arg1, '0'
  rcall lcd_character
  rjmp _menu_loop_keypad_poll

; -----------------------------------------------------------------------------

_menu_loop_keypad_poll:
  rcall keypad_poll
  cpi arg1, '*'
  brne _menu_loop_keypad_poll_action
  inc var1
  cpi var1, MENU_ENTRY_END
  brge _menu_loop_wrap
  rjmp menu_loop
_menu_loop_wrap:
  clr var1
  rjmp menu_loop

_menu_loop_keypad_poll_action:
  cpi arg1, '#'
  breq _menu_loop_action_clock_set
  rjmp menu_loop

_menu_loop_action_clock_set:
  cpi var1, MENU_ENTRY_CLOCK_SET
  brne _menu_loop_action_alarm_set
  rcall clock_set_action
  rjmp menu_loop

_menu_loop_action_alarm_set:
  cpi var1, MENU_ENTRY_ALARM_SET
  brne _menu_loop_action_alarm_enable
  rcall alarm_set_action
  rjmp menu_loop

_menu_loop_action_alarm_enable:
  cpi var1, MENU_ENTRY_ALARM_ENABLE
  brne _menu_loop_action_buzzer_status
  rcall alarm_enable_action
  rjmp menu_loop

_menu_loop_action_buzzer_status:
  cpi var1, MENU_ENTRY_BUZZER_STATUS
  brne _menu_loop_end
  rcall buzzer_status_action
  rjmp menu_loop

_menu_loop_end:
  rjmp menu_loop

; =============================================================================
;  CLOCK SET SUB-MENU
; =============================================================================

; clock_set_action -- Adjust time of the clock.
; IN: N/A
; OUT: N/A
clock_set_action:
  push var1
  cli ; Freeze time while adjusting clock.
  clr var1 ; Holds current digit to be adjusted (0 to 5).

_clock_set_loop:
  mov arg1, var1
  rcall clock_set_display

  rcall keypad_poll

  ; Check if action should be aborted.
  cpi arg1, '#'
  breq _clock_set_end
  cpi arg1, '*'
  breq _clock_set_end

  ; Check a key between '0' and '9' is pressed, only then perform adjust.
  cpi arg1, 0x30
  brlt _clock_set_loop
  cpi arg1, 0x40
  brge _clock_set_loop
  mov arg2, var1
  rcall clock_set_update_digit

  inc var1
  cpi var1, CLOCK_SET_END
  breq _clock_set_end
  rjmp _clock_set_loop

_clock_set_end:
  sei ; Un-freeze time.
  pop var1
  ret

; -----------------------------------------------------------------------------

; clock_set_update_digit -- Update individual digit on clock manually.
; IN: arg1 -> ASCII value of digit to update.
;     arg2 -> Digit location from 0 to 5.
; OUT: N/A
clock_set_update_digit:
  subi arg1, 0x30 ; Convert from ASCII to actual integer value.

  cpi arg2, CLOCK_SET_HOUR_MSD
  brne _clock_set_update_hour_lsd
  rcall convert_to_msd
  mov hour, arg1
  ret

_clock_set_update_hour_lsd:
  cpi arg2, CLOCK_SET_HOUR_LSD
  brne _clock_set_update_minute_msd
  add hour, arg1
  ret

_clock_set_update_minute_msd:
  cpi arg2, CLOCK_SET_MINUTE_MSD
  brne _clock_set_update_minute_lsd
  rcall convert_to_msd
  mov minute, arg1
  ret

_clock_set_update_minute_lsd:
  cpi arg2, CLOCK_SET_MINUTE_LSD
  brne _clock_set_update_second_msd
  add minute, arg1
  ret

_clock_set_update_second_msd:
  cpi arg2, CLOCK_SET_SECOND_MSD
  brne _clock_set_update_second_lsd
  rcall convert_to_msd
  mov second, arg1
  ret

_clock_set_update_second_lsd:
  cpi arg2, CLOCK_SET_SECOND_LSD
  brne _clock_set_update_digit_end
  add second, arg1
  ret

_clock_set_update_digit_end:
  ret

; -----------------------------------------------------------------------------

; clock_set_display -- Display "update in progress" clock.
; IN: arg1 -> Digit location from 0 to 5. (What will be updated next.)
; OUT: N/A
clock_set_display:
  push var1
  mov var1, arg1

  rcall lcd_first_position

  mov arg1, hour
  rcall split_msd_lsd
  subi arg1, -0x30
  subi arg2, -0x30

  cpi var1, CLOCK_SET_HOUR_MSD
  brne _clock_set_display_hour_msd
  ldi arg1, '_' ; Use underscore to display "current" number to update.
_clock_set_display_hour_msd:
  rcall lcd_character

  mov arg1, arg2
  cpi var1, CLOCK_SET_HOUR_LSD
  brne _clock_set_display_hour_lsd
  ldi arg1, '_'

_clock_set_display_hour_lsd:
  rcall lcd_character

  ldi arg1, ':'
  rcall lcd_character

  mov arg1, minute
  rcall split_msd_lsd
  subi arg1, -0x30
  subi arg2, -0x30

  cpi var1, CLOCK_SET_MINUTE_MSD
  brne _clock_set_display_minute_msd
  ldi arg1, '_'
_clock_set_display_minute_msd:
  rcall lcd_character

  mov arg1, arg2
  cpi var1, CLOCK_SET_MINUTE_LSD
  brne _clock_set_display_minute_lsd
  ldi arg1, '_'

_clock_set_display_minute_lsd:
  rcall lcd_character

  ldi arg1, ':'
  rcall lcd_character

  mov arg1, second
  rcall split_msd_lsd
  subi arg1, -0x30
  subi arg2, -0x30

  cpi var1, CLOCK_SET_SECOND_MSD
  brne _clock_set_display_second_msd
  ldi arg1, '_'
_clock_set_display_second_msd:
  rcall lcd_character

  mov arg1, arg2
  cpi var1, CLOCK_SET_SECOND_LSD
  brne _clock_set_display_second_lsd
  ldi arg1, '_'

_clock_set_display_second_lsd:
  rcall lcd_character

  pop var1
  ret

; =============================================================================
;  ALARM SET SUB-MENU
; =============================================================================

; alarm_set_action -- Adjust the time of the alarm.
; IN: N/A
; OUT: N/A
alarm_set_action:
  push var1
  clr var1 ; Holds current digit to be adjusted (0 to 5).

_alarm_set_loop:
  mov arg1, var1
  rcall alarm_set_display

  rcall keypad_poll

  ; Check if action should be aborted.
  cpi arg1, '#'
  breq _alarm_set_end
  cpi arg1, '*'
  breq _alarm_set_end

  ; Check a key between '0' and '9' is pressed, only then perform adjust.
  cpi arg1, 0x30
  brlt _alarm_set_loop
  cpi arg1, 0x40
  brge _alarm_set_loop
  mov arg2, var1
  rcall alarm_set_update_digit

  inc var1
  cpi var1, ALARM_SET_END
  breq _alarm_set_end
  rjmp _alarm_set_loop

_alarm_set_end:
  pop var1
  ret

; -----------------------------------------------------------------------------

; alarm_set_update_digit -- Update individual digit on alarm.
; IN: arg1 -> ASCII value of digit to update.
;     arg2 -> Digit location from 0 to 5.
; OUT: N/A
alarm_set_update_digit:
  subi arg1, 0x30 ; Convert from ASCII to actual integer value.

  cpi arg2, ALARM_SET_HOUR_MSD
  brne _alarm_set_update_hour_lsd
  rcall convert_to_msd
  mov alarm_hour, arg1
  ret

_alarm_set_update_hour_lsd:
  cpi arg2, ALARM_SET_HOUR_LSD
  brne _alarm_set_update_minute_msd
  add alarm_hour, arg1
  ret

_alarm_set_update_minute_msd:
  cpi arg2, ALARM_SET_MINUTE_MSD
  brne _alarm_set_update_minute_lsd
  rcall convert_to_msd
  mov alarm_minute, arg1
  ret

_alarm_set_update_minute_lsd:
  cpi arg2, ALARM_SET_MINUTE_LSD
  brne _alarm_set_update_digit_end
  add alarm_minute, arg1
  ret

_alarm_set_update_digit_end:
  ret

; -----------------------------------------------------------------------------

; alarm_set_display -- Display "update in progress" for alarm.
; IN: arg1 -> Digit location from 0 to 5. (What will be updated next.)
; OUT: N/A
alarm_set_display:
  push var1
  mov var1, arg1

  rcall lcd_first_position

  mov arg1, alarm_hour
  rcall split_msd_lsd
  subi arg1, -0x30
  subi arg2, -0x30

  cpi var1, ALARM_SET_HOUR_MSD
  brne _alarm_set_display_hour_msd
  ldi arg1, '_' ; Use underscore to display "current" number to update.
_alarm_set_display_hour_msd:
  rcall lcd_character

  mov arg1, arg2
  cpi var1, ALARM_SET_HOUR_LSD
  brne _alarm_set_display_hour_lsd
  ldi arg1, '_'

_alarm_set_display_hour_lsd:
  rcall lcd_character

  ldi arg1, ':'
  rcall lcd_character

  mov arg1, alarm_minute
  rcall split_msd_lsd
  subi arg1, -0x30
  subi arg2, -0x30

  cpi var1, ALARM_SET_MINUTE_MSD
  brne _alarm_set_display_minute_msd
  ldi arg1, '_'
_alarm_set_display_minute_msd:
  rcall lcd_character

  mov arg1, arg2
  cpi var1, ALARM_SET_MINUTE_LSD
  brne _alarm_set_display_minute_lsd
  ldi arg1, '_'

_alarm_set_display_minute_lsd:
  rcall lcd_character

  ldi arg1, ' '
  rcall lcd_character
  rcall lcd_character
  rcall lcd_character

  pop var1
  ret

; =============================================================================
;  ALARM STATUS SUB-MENU
; =============================================================================

; alarm_enable_action -- Toggle "alarm_active" flag.
; IN: N/A
; OUT: N/A
alarm_enable_action:
  push var1

  tst alarm_active
  breq _alarm_enable_action_not_active
  clr var1
  mov alarm_active, var1

  pop var1
  ret

_alarm_enable_action_not_active:
  ser var1
  mov alarm_active, var1

  pop var1
  ret

; =============================================================================
;  BUZZER STATUS SUB-MENU
; =============================================================================

; buzzer_status_action -- Toggle buzzer.
; IN: N/A
; OUT: N/A
buzzer_status_action:
  rcall buzzer_is_active
  tst arg1
  breq _buzzer_status_action_not_active
  rcall buzzer_deactivate
  ret
_buzzer_status_action_not_active:
  rcall buzzer_activate
  ret

; =============================================================================
;  LCD DISPLAY INTERFACE
; =============================================================================

; lcd_init -- Initialize LCD display.
; IN: N/A
; OUT: N/A
lcd_init:
  push var1

  ; Wait a little after MC startup.
  rcall long_delay

  ; Set 4-bit data mode. (Directly, since interfaces does not work yet.)
  ldi var1, 0b00000010
  out PORTB, var1
  sbi PORTB, 4 ; Set enable.
  rcall short_delay
  cbi PORTB, 4 ; Clear enable.
  rcall long_delay

  ; Trick to keep to keep brightness correct.
  ldi arg1, 0b10001000
  rcall lcd_command
  ldi arg1, '!'
  rcall lcd_character

  ; Set 1-line mode.
  ldi arg1, 0b00100000
  rcall lcd_command

  ; Turn on LCD. (Without blinking cursor.)
  ldi arg1, 0b00001100
  rcall lcd_command

  ; Clear whole display.
  ldi arg1, 0b00000001 
  rcall lcd_command
  rcall long_delay

  ; Set cursor to move left after write operation.
  ldi arg1, 0b00000110
  rcall lcd_command

  pop var1
  ret

; -----------------------------------------------------------------------------

; lcd_character -- Print single character (extended ASCII) to LCD display.
; IN: arg1 -> character
; OUT: N/A
lcd_character:
  push var1

  ; Extract high nibble from byte.
  mov var1, arg1
  swap var1 ; Swap nibbles.
  andi var1, 0xf ; Clear high nibble.
  out PORTB, var1
  sbi PORTB, 5 ; Set RS to 1 to use character register.

  ; Toggle enable to send the first 4 bits of data.
  sbi PORTB, 4
  rcall short_delay
  cbi PORTB, 4
  rcall short_delay
  
  ; Extract lower nibble from byte.
  mov var1, arg1
  andi var1, 0xf
  out PORTB, var1
  sbi PORTB, 5

  ; Toggle enable to send the second 4 bits of data.
  sbi PORTB, 4
  rcall short_delay
  cbi PORTB, 4
  rcall short_delay

  pop var1
  ret

; -----------------------------------------------------------------------------

; lcd_command -- Send control command to LCD display.
; IN: arg1 -> command number
; OUT: N/A
lcd_command:
  push var1

  ; Extract high nibble from byte.
  mov var1, arg1
  swap var1 ; Swap nibbles.
  andi var1, 0xf ; Clear high nibble.
  out PORTB, var1
  ; RS remains 0 to use command register.

  ; Toggle enable to send the first 4 bits of data.
  sbi PORTB, 4
  rcall short_delay
  cbi PORTB, 4
  rcall short_delay
  
  ; Extract lower nibble from byte.
  mov var1, arg1
  andi var1, 0xf
  out PORTB, var1

  ; Toggle enable to send the second 4 bits of data.
  sbi PORTB, 4
  rcall short_delay
  cbi PORTB, 4
  rcall short_delay

  pop var1
  ret

; -----------------------------------------------------------------------------

; lcd_first_position -- Set LCD address to first position (0).
; IN: N/A
; OUT: N/A
lcd_first_position:
  ldi arg1, 0b10000000 ; Set DDRAM address to first display position.
  rcall lcd_command
  ret

; -----------------------------------------------------------------------------

; lcd_number -- Print two digit number to LCD display.
; IN: arg1 -> MSD of two digit number.
;     arg2 -> LSD of two digit number.
; OUT: N/A
lcd_number:
  ; Add ASCII compensator.
  subi arg1, -0x30
  subi arg2, -0x30

  rcall lcd_character
  mov arg1, arg2
  rcall lcd_character

  ret

; -----------------------------------------------------------------------------

; lcd_eeprom_string -- Print string from EEPROM to LCD display.
; IN: arg1 -> Start of EEPROM address.
;     arg2 -> Amount of characters in string.
; OUT: N/A
lcd_eeprom_string:
  push var1
  push var2
  mov var1, arg1
  mov var2, arg2

_lcd_eeprom_string_loop:
  mov arg1, var1
  rcall eeprom_read
  rcall lcd_character
  inc var1 ; Address incremented for every cycle.
  dec var2 ; Will be decremented until zero.
  tst var2
  brne _lcd_eeprom_string_loop

  pop var2
  pop var1
  ret

; =============================================================================
;  KEYPAD INTERFACE
; =============================================================================

; keypad_poll -- Retrive next keypress from keypad.
; IN: N/A
; OUT: arg1 -> Pressed key in ASCII, or 0 if no new key has been pressed.
keypad_poll:
  push var1
  clr arg1

  ; ldi var1, 0b00000100 ; K1
  ; out PORTD, var1
  sbi PORTD, 2 ; K1
  nop ; This NOP is required for correct timing.
  sbic PIND, 1 ; R1
  ldi arg1, '1'
  sbic PIND, 6 ; R2
  ldi arg1, '4'
  sbic PIND, 5 ; R3
  ldi arg1, '7'
  sbic PIND, 3 ; R4
  ldi arg1, '*'
  cbi PORTD, 2 ; K1

  ; ldi var1, 0b00000001 ; K2
  ; out PORTD, var1
  sbi PORTD, 0 ; K2
  nop
  sbic PIND, 1 ; R1
  ldi arg1, '2'
  sbic PIND, 6 ; R2
  ldi arg1, '5'
  sbic PIND, 5 ; R3
  ldi arg1, '8'
  sbic PIND, 3 ; R4
  ldi arg1, '0'
  cbi PORTD, 0 ; K2

  ; ldi var1, 0b00010000 ; K3
  ; out PORTD, var1
  sbi PORTD, 4 ; K3
  nop
  sbic PIND, 1 ; R1
  ldi arg1, '3'
  sbic PIND, 6 ; R2
  ldi arg1, '6'
  sbic PIND, 5 ; R3
  ldi arg1, '9'
  sbic PIND, 3 ; R4
  ldi arg1, '#'
  cbi PORTD, 4 ; K3

  lds var1, _keypad_key_previous
  cp arg1, var1
  brne _keypad_return_new_key
  clr arg1
  pop var1
  ret ; Return 0 to indicate NO new key.

_keypad_return_new_key:
  sts _keypad_key_previous, arg1
  pop var1
  ret

; =============================================================================
;  BUZZER INTERFACE
; =============================================================================

; buzzer_activate -- Start buzzer.
; IN: N/A
; OUT: N/A
buzzer_activate:
  sbi PORTD, 7
  ret

; -----------------------------------------------------------------------------

; buzzer_deactivate -- Stop buzzer.
; IN: N/A
; OUT: N/A
buzzer_deactivate:
  cbi PORTD, 7
  ret

; -----------------------------------------------------------------------------

; buzzer_is_active -- Fetch current status of buzzer (output).
; IN: N/A
; OUT: arg1 -> 1 if active, or 0 if deactivated.
buzzer_is_active:
  ldi arg1, 0
  sbic PORTD, 7
  ldi arg1, 1
  ret

; =============================================================================
;  COMMON ROUTINES
; =============================================================================

; eeprom_read -- Read from EEPROM.
; IN: arg1 -> EEPROM address
; OUT: arg1 -> value
eeprom_read:
  sbic EECR, 1 ; Check if EEPROM is busy.
  rjmp eeprom_read
  out EEARL, arg1 ; Select EEPROM address register.
  ldi arg1, 0
  out EEARH, arg1
  sbi EECR, EERE ; Activate "Read Enable".
  in arg1, EEDR ; Read from data register.
  ret

; -----------------------------------------------------------------------------

; long_delay -- Waste CPU cycles to cause a "long" delay.
; IN: N/A
; OUT: N/A
long_delay:
  push var1
  push var2
_long_delay_loop:
  dec var1
  brne _long_delay_loop
  dec var2
  brne _long_delay_loop
  pop var2
  pop var1
  ret

; -----------------------------------------------------------------------------

; short_delay -- Waste CPU cycles to cause a "short" delay.
; IN: N/A
; OUT: N/A
short_delay:
  push var1
_short_delay_loop:
  dec var1
  brne _short_delay_loop
  pop var1
  ret

; -----------------------------------------------------------------------------

; split_msd_lsd -- Split two digit number into MSD and LSD.
; IN: arg1 -> Two digit number.
; OUT: arg1 -> MSD
;      arg2 -> LSD
split_msd_lsd:
  mov arg2, arg1
  ldi arg1, -1
_split_msd_lsd_positive:
  inc arg1
  subi arg2, 10
  brpl _split_msd_lsd_positive
  subi arg2, -10 ; Actually same as "addi arg1, 10", which does not exist.
  ret

; -----------------------------------------------------------------------------

; convert_to_lsd -- Convert LSD to MSD.
; IN: arg1 -> LSD
; OUT: arg1 -> MSD
convert_to_msd:
  push var1
  mov var1, arg1
_convert_to_msd_loop:
  cpi var1, 0
  breq _convert_to_msd_done
  subi arg1, -9
  dec var1
  rjmp _convert_to_msd_loop
_convert_to_msd_done:
  pop var1
  ret

; =============================================================================
;  INTERRUPT ROUTINES
; =============================================================================

; timer1_interrupt -- Updates clock (second/minute/hour) counters.
; IN: N/A
; OUT: N/A
timer1_interrupt:
  inc second
  ldi intvar, 60
  cp second, intvar
  brlt _timer1_check_alarm
  clr second
  inc minute
  cp minute, intvar
  brlt _timer1_check_alarm
  clr minute
  inc hour
  ldi intvar, 24
  cp hour, intvar
  brlt _timer1_check_alarm
  clr hour

_timer1_check_alarm:
  tst alarm_active
  breq _timer1_done

  cp alarm_hour, hour
  brne _timer1_done
  cp alarm_minute, minute
  brne _timer1_done

  ; Clear alarm active bit, to prevent triggering again.
  clr intvar
  mov alarm_active, intvar

  ; Set port directly, since buzzer_activate cannot be called from interrupt.
  sbi PORTD, 7

_timer1_done:
  reti
          


Topic: Scripts and Code, by Kjetil @ 07/08-2016, Article Link

Steam on Slackware 14.2

Slackware 14.2 was recently released, so I decided to attempt a Steam installation on it, which succeeded in the end.

Here are the steps I followed:
1) Download and install the 64-bit version of Slackware 14.2. You can get it here: http://www.slackware.com/getslack/torrents.php
2) Setup "multilib", so the system is able to run 32-bit programs. Use this guide: http://www.slackware.com/~alien/multilib/.
3) Install OpenAL, which is a dependency for Steam. I recommend to use SlackBuilds: https://slackbuilds.org/repository/14.2/libraries/OpenAL/
4) Install Steam. Also easy with SlackBuilds: https://slackbuilds.org/repository/14.2/games/steam/
5) Install a proprietary graphics driver, for better OpenGL performance.

It is important to install a 64-bit base system because some of the games on Steam are in fact only 64-bit. However, Steam itself and certain other games are only 32-bit, hence the need for multilib. I use an nVidia graphics card, and this had to be installed AFTER multilib had been set up, so I recommend doing the same.

I have done only minor testing so far, but I am still a bit sceptical about the OpenAL dependency. It seems that some games bundle their own OpenAL libraries, so they aren't always needed. It is also possible that a 32-bit version of the OpenAL library needs to be installed for certain stuff to work.

Topic: Configuration, by Kjetil @ 07/07-2016, Article Link

SDL-Man for SDL2

Eight years ago I released a Pac-Man clone called SDL-Man. Now someone has expressed interest and asked me to update the code to use the newer SDL2 library instead of the older SDL1 library. And so I did. The changes are mostly related to the initialization and handling of video and graphics, which has changed a lot with SDL2.

The source code is still released under GPL3 and can be found here. As before, the main SDL2 and SDL2_mixer libraries are required.

Topic: Open Source, by Kjetil @ 11/06-2016, Article Link

D64 PRG Dumper

Maybe such a program already exists, but I do not know of any, so I made my own. This program scans a D64 file, which is a disk image typically used by Commodore 64 emulators, and attempts to extract all programs (PRG files) from it.

Take a look:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <limits.h>
#include <errno.h>
#include <ctype.h>

#define SECTOR_SIZE 256
#define FIRST_DIR_TRACK 18
#define FIRST_DIR_SECTOR 1
#define DIR_ENTRIES_PER_SECTOR 8
#define FILE_NAME_SIZE 16

#pragma pack(1)
typedef struct dir_entry_s {
  uint8_t next_dir_track;
  uint8_t next_dir_sector;
  uint8_t file_type;
  uint8_t first_file_track;
  uint8_t first_file_sector;
  uint8_t petscii_file_name[FILE_NAME_SIZE];
  uint8_t extra[11]; /* Unused by this program. */
} dir_entry_t;
#pragma pack()

static int byte_offset(int track_no, int sector_no)
{
  int offset = 0;

  while (track_no-- > 0) {
    if (track_no >= 31) {
      offset += 17;
    } else if (track_no >= 25) {
      offset += 18; 
    } else if (track_no >= 18) {
      offset += 19;
    } else if (track_no >= 1) {
      offset += 21;
    }
  }

  return (offset + sector_no) * SECTOR_SIZE;
}

static void dump_program(FILE *d64, char *name, int first_track, int first_sector)
{
  uint8_t file_track, file_sector;
  uint8_t data[254];
  int size, saved = 0;
  char path[PATH_MAX];
  FILE *prg;

  snprintf(path, PATH_MAX, "%s.prg", name);
  prg = fopen(path, "wbx");
  if (prg == NULL) {
    fprintf(stderr, "Warning: fopen(%s) failed: %s\n", path, strerror(errno));
    return;
  }

  file_track = first_track;
  file_sector = first_sector;
  do {
    if (fseek(d64, byte_offset(file_track, file_sector), SEEK_SET) == -1) {
      fprintf(stderr, "Warning: fseek(%d,%d) failed: %s\n", file_track, file_sector, strerror(errno));
      fclose(prg);
      return;
    }

    size =  fread(&file_track, sizeof(uint8_t), 1, d64);
    size += fread(&file_sector, sizeof(uint8_t), 1, d64);
    size += fread(&data, sizeof(uint8_t), 254, d64);
    if (size != SECTOR_SIZE) {
      fprintf(stderr, "Warning: fread() read less than a sector for program: \"%s\"\n", name);
      fclose(prg);
      return;
    }

    saved += fwrite(&data, sizeof(uint8_t), 254, prg);

    /* NOTE: No protection against circular references. */
  } while (file_track != 0);

  printf("Extracted: \"%s\" (%d bytes)\n", name, saved);
  fclose(prg);
}

int main(int argc, char *argv[])
{
  FILE *d64;
  int i, n, size;
  uint8_t dir_track, dir_sector, petscii;
  char ascii_name[FILE_NAME_SIZE + 1];
  dir_entry_t de[DIR_ENTRIES_PER_SECTOR];

  if (argc != 2) {
    fprintf(stderr, "Usage: %s <d64 image>\n", argv[0]);
    return 1;
  }

  d64 = fopen(argv[1], "rb");
  if (d64 == NULL) {
    fprintf(stderr, "Error: fopen(%s) failed: %s\n", argv[1], strerror(errno));
    return 1;
  }

  /* Traverse directory entries: */
  dir_track = FIRST_DIR_TRACK;
  dir_sector = FIRST_DIR_SECTOR;
  do {
    if (fseek(d64, byte_offset(dir_track, dir_sector), SEEK_SET) == -1) {
      fprintf(stderr, "Error: fseek(%d,%d) failed: %s\n", dir_track, dir_sector, strerror(errno));
      return 1;
    }
    size = fread(&de[0], sizeof(dir_entry_t), DIR_ENTRIES_PER_SECTOR, d64);
    if (size != DIR_ENTRIES_PER_SECTOR) {
      fprintf(stderr, "Error: fread() read less than a sector for directory entry\n");
      return 1;
    }

    for (i = 0; i < DIR_ENTRIES_PER_SECTOR; i++) {
      /* Only extract if PRG type, meaning bit 2 is set: */
      if ((de[i].file_type & 0x2) == 0)
        continue;

      /* Convert file name from PETSCII to ASCII: */
      memset(ascii_name, '\0', FILE_NAME_SIZE + 1);
      for (n = 0; n < FILE_NAME_SIZE; n++) {
        petscii = de[i].petscii_file_name[n];
        if (petscii == 0xA0) /* Padding, end of name. */
          break;
        if (isalnum(petscii)) {
          ascii_name[n] = tolower(petscii);
        } else if (petscii == ' ') {
          ascii_name[n] = '_';
        } else {
          ascii_name[n] = '.';
        }
      }

      /* Dump the program unless 0: */
      if (de[i].first_file_track == 0)
        continue;
      dump_program(d64, ascii_name, de[i].first_file_track, de[i].first_file_sector);
    }

    dir_track = de[0].next_dir_track;
    dir_sector = de[0].next_dir_sector;

    /* NOTE: No protection against circular references. */
  } while (dir_track != 0);

  fclose(d64);
  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 12/05-2016, Article Link

LUKS Hidden by FAT32

Here is a trick to hide the existence of an encrypted LUKS partition on a disk, by offsetting it into the "data region" of a FAT32 partition. When the disk is inserted on Windows, it will simply show up as an regular empty drive. Putting data on it when mounted as a FAT32 (e.g. on Windows) will most likely destroy the encrypted information.

To find the data region of a FAT32 partition, I have developed this tool based on earlier work on VBRs:

#include <stdio.h>
#include <stdint.h>

#pragma pack(1)
typedef struct vbr_s {
  uint8_t jump_code[3];
  char oem_id[8];
  uint16_t bytes_per_sector;
  uint8_t sectors_per_cluster;
  uint16_t reserved_sectors;
  uint8_t no_of_fats;
  uint16_t root_entries;
  uint16_t small_sectors;
  uint8_t media_descriptor;
  uint16_t sectors_per_fat;
  uint16_t sectors_per_track;
  uint16_t heads;
  uint32_t hidden_sectors;
  uint32_t large_sectors;
  uint8_t physical_drive_number;
  uint8_t current_head;
  uint8_t signature;
  uint32_t id;
  char volume_label[11];
  char system_id[8];
  uint8_t boot_code[448];
  uint16_t boot_record_signature;
} vbr_t;
#pragma pack()

int main(int argc, char *argv[])
{
  FILE *fh;
  int bytes;
  uint8_t sector[512];
  vbr_t *vbr;

  int root_dir_sectors;
  int fat_part;
  int fat_size;
  int first_data_sector;

  if (argc != 2) {
    fprintf(stderr, "Usage: %s <device>\n", argv[0]);
    return 1;
  }

  fh = fopen(argv[1], "rb");
  if (fh == NULL) {
    fprintf(stderr, "Error: Unable to open device for reading.\n");
    return 1;
  }

  bytes = fread(&sector, sizeof(uint8_t), 512, fh);
  fclose(fh);

  if (bytes < 512) {
    fprintf(stderr, "Error: File is less than 512 bytes.\n");
    return 1;
  }

  vbr = (vbr_t *)&sector[0];

  if (vbr->bytes_per_sector == 0 ||
      vbr->sectors_per_cluster == 0 ||
      vbr->no_of_fats == 0) {
    fprintf(stderr, "Error: Invalid boot record.\n");
    return 1;
  }

  root_dir_sectors = ((vbr->root_entries * 32) + 
    (vbr->bytes_per_sector - 1)) /
     vbr->bytes_per_sector;

  fat_part = ((256 * vbr->sectors_per_cluster) + vbr->no_of_fats) / 2;

  fat_size = ((vbr->large_sectors -
              (vbr->reserved_sectors +
               root_dir_sectors)) + (fat_part - 1)) / fat_part;

  first_data_sector = vbr->reserved_sectors +
                     (vbr->no_of_fats * fat_size) + 
                      root_dir_sectors;

  printf("%d\n", first_data_sector * vbr->bytes_per_sector);
  return 0;
}
          


Here are step by step instructions on creating it all:
(Assuming the tool mentioned above is compiled as "fds" and available.)

fdisk /dev/sdd # Create one single large FAT32 partition (0xb) as /dev/sdd1.
mkfs.vfat /dev/sdd1
losetup -o `./fds /dev/sdd1` /dev/loop0 /dev/sdd1
cryptsetup -v luksFormat /dev/loop0
cryptsetup luksOpen /dev/loop0 luks
mkfs.ext2 /dev/mapper/luks
cryptsetup luksClose luks
losetup -d /dev/loop0
          


Mounting the hidden LUKS partition later on:

losetup -o `./fds /dev/sdd1` /dev/loop0 /dev/sdd1
cryptsetup luksOpen /dev/loop0 luks
mount /dev/mapper/luks /mnt/luks
          


And unmounting it:

umount /mnt/luks
cryptsetup luksClose luks
losetup -d /dev/loop0
          


It may be possible to further hide the existence on the LUKS partition by also placing the LUKS header elsewhere, as is allegedly supported in newer versions of the "cryptsetup" tool.

Topic: Scripts and Code, by Kjetil @ 01/03-2016, Article Link

Directory Tree Diff Front End

You may recall the Directory Tree Diff program I presented earlier. This is a new and improved version of that program. Instead of just dumping the differences, they are now displayed through a curses-based interface, which can be navigated. The program can also start the regular "diff" program on any different files found.

Have a look:

Screenshot of new difftree.


The source code is released under the MIT License here.

Topic: Open Source, by Kjetil @ 06/02-2016, Article Link

Monitoring File Open in a Directory

Here is part of an experiment I have been working on. I did not find any use for this (yet), but the method is worth considering. The following program uses the Linux kernel's "inotify" mechanism to print a message whenever a file is opened in a specific directory.

Have a look:

#include <dirent.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/inotify.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

#define WATCH_FILES_MAX 256

typedef struct watch_s {
  int wd;
  char file[PATH_MAX];
} watch_t;

static watch_t watch[WATCH_FILES_MAX];
static int watch_n = 0;
static int ifd;

static int watch_files(char *dir)
{
  struct dirent *entry;
  struct stat st;
  DIR *dh;
  char path[PATH_MAX];

  ifd = inotify_init();
  if (ifd == -1) {
    fprintf(stderr, "inotify_init() failed: %s\n", strerror(errno));
    return -1;
  }

  dh = opendir(dir);
  if (dh == NULL) {
    fprintf(stderr, "opendir(%s) failed: %s\n", dir, strerror(errno));
    return -1;
  }

  watch_n = 0;
  while ((entry = readdir(dh))) {
    if (entry->d_name[0] == '.')
      continue;

    snprintf(path, PATH_MAX, "%s/%s", dir, entry->d_name);

    if (stat(path, &st) == -1) {
      fprintf(stderr, "stat(%s) failed: %s\n", path, strerror(errno));
      closedir(dh);
      return -1;
    }

    if (S_ISREG(st.st_mode)) {
      watch[watch_n].wd = inotify_add_watch(ifd, path, IN_OPEN);
      if (watch[watch_n].wd == -1) {
        fprintf(stderr, "inotify_add_watch(%s) failed: %s\n", path, strerror(errno));
        closedir(dh);
        return -1;
      }

      strncpy(watch[watch_n].file, entry->d_name, PATH_MAX);
      fprintf(stderr, "Watching: %s\n", entry->d_name);

      watch_n++;
      if (watch_n >= WATCH_FILES_MAX) {
        fprintf(stderr, "Max files reached, skipping the rest!\n");
        break;
      }
    }
  }

  closedir(dh);
  return 0;
}

int main(int argc, char *argv[])
{
  struct inotify_event iev;
  time_t now;
  int i;

  if (argc != 2) {
    fprintf(stderr, "Usage: %s <watch directory>\n", argv[0]);
    return 1;
  }

  if (watch_files(argv[1]) != 0) {
    return 1;
  }

  setlinebuf(stdout);

  while (1) {
    if (read(ifd, &iev, sizeof(struct inotify_event)) > 0) {
      for (i = 0; i < watch_n; i++) {
        if (iev.wd == watch[i].wd) {
          time(&now);
          fprintf(stdout, "%s @ %s", watch[i].file, ctime(&now));
        }
      }
    }
  }

  /* NOTE: No file descriptors explicitly closed. */
  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 09/01-2016, Article Link