# Library for interfacing with an MCP23008 chip, as used on the jeelabs
# expander and output plugs

namespace eval jeeio {
    package require piio
    namespace path ::twowire

    variable slaveaddr 0x20
    variable regs {
	iodir 0 ipol 1 gpinten 2 defval 3 
	intcon 4 iocon 5 gppu 6 intf 7
	intcap 8 gpio 9 olat 10
    }

    namespace ensemble create -subcommands {
	expanderplug outputplug setoutput setinput readpin writepin
	writeddr writegpio readgpio
    }

    proc readreg {fd reg} {
	variable regs
	return [readregbyte $fd [dict get $regs $reg]]
    }

    proc writereg {fd reg val} {
	variable regs
	writeregbyte $fd [dict get $regs $reg] $val
    }

    proc expanderplug {bus {num 0}} {
	variable slaveaddr
	return [twowire $bus [expr {$slaveaddr + $num}]]
    }

    proc outputplug {bus {num 0}} {
	variable slaveaddr
	set fd [twowire $bus [expr {$slaveaddr + 6 + $num}]]
	# Configure all pins as outputs
	writeddr $fd 0
	return $fd
    }

    proc motorplug {bus {num 0}} {
	variable slaveaddr
	set fd [twowire $bus [expr {$slaveaddr + 6 + $num}]]
	# Configure pin 0 through 3 as outputs
	set ddr [readreg $fd iodir]
	writereg $fd iodir [expr {$ddr & ~0xf}]
	return $fd
    }

    # Configure the function of all 8 pins
    proc writeddr {fd val} {
	writereg $fd iodir $val
    }

    # Set all outputs at once
    proc writegpio {fd val} {
	writereg $fd olat $val
    }

    # Read the state of all pins
    proc readgpio {fd} {
	return [readreg $fd gpio]
    }

    # Configure a single pin as output
    proc setoutput {fd pin} {
	set ddr [readreg $fd iodir]
	writereg $fd iodir [expr {$ddr & ~(1 << $pin)}]
    }

    # Configure a single pin as input
    proc setinput {fd pin} {
	set ddr [readreg $fd iodir]
	writereg $fd iodir [expr {$ddr | (1 << $pin)}]
    }

    # Read the state of a single pin
    proc readpin {fd pin} {
	return [expr {([readreg $fd gpio] >> $pin) & 1}]
    }

    # Set the state of a single pin
    proc writepin {fd pin state} {
	set olat [readreg $fd olat]
	set new [expr {$state ? $olat | (1 << $pin) : $olat & ~(1 << $pin)}]
	writereg $fd gpio $new
    }

    ### LCD plug related commands
    # Pins 0-3 are the data bits
    # Pin 4 is RS (Register select)
    # Pin 5 is SW (Switch)
    # Pin 6 is E (Enable)
    # Pin 7 is BL (Backlight)

    proc lcdplug {bus {rows 2} {cols 16}} {
	variable slaveaddr
	variable lcd
	set fd [twowire $bus [expr {$slaveaddr + 4}]]
	# Prepare all outputs to start out low
	writereg $fd olat 0
	# Set all pins as output, except for pin 5
	writereg $fd iodir [expr {1 << 5}]
	# There's no way to know what state the device is in, so we have to do
	# a full "initialization by instruction"
	# Set the device to 8-bit interface
	lcdwrite $fd 0x03
	piio usleep 4100
	lcdwrite $fd 0x03
	piio usleep 100
	lcdwrite $fd 0x03
	piio usleep 37
	# Set the device to 4-bit interface
	lcdwrite $fd 0x02
	piio usleep 37
	# Configure the number of display lines and character font
	command $fd 0x2C
	piio usleep 37
	# Store the display size
	dict set lcd $fd [dict create rows $rows cols $cols]
	return $fd
    }

    proc lcdwrite {fd val} {
	# Write the data with the enable bit set
	writereg $fd gpio [expr {$val | 0x40}]
	# Clear the enable bit
	writereg $fd gpio [expr {$val & ~0x40}]
    }

    proc command {fd byte} {
	lcdwrite $fd [expr {$byte >> 4}]
	lcdwrite $fd [expr {$byte & 0xf}]
    }

    proc clear {fd} {
	command $fd 1
    }

    proc home {fd} {
	command $fd 2
    }

    proc cursor {fd row col} {
	set offset [expr {($row & 1 ? 64 : 0) + ($row & 2 ? 20 : 0)}]
	command $fd [expr {0x80 + $offset + $col}]
    }

    proc print {fd str} {
	binary scan $str cu* chars
	foreach byte $chars {
	    lcdwrite $fd [expr {($byte >> 4) | 0x10}]
	    lcdwrite $fd [expr {($byte & 0xf) | 0x10}]
	    # Wait the required time between characters
	    piio usleep 41
	}
    }
}
