Browse Source

add LiquidCrystal lua module (#2974)

seregaxvm 4 years ago
parent
commit
6499387039

+ 559 - 0
docs/lua-modules/liquidcrystal.md

@@ -0,0 +1,559 @@
+# LiquidCrystal Module
+| Since  | Origin / Contributor  | Maintainer  | Source  |
+| :----- | :-------------------- | :---------- | :------ |
+| 2019-12-01 | [Matsievskiy Sergey](https://github.com/seregaxvm) | [Matsievskiy Sergey](https://github.com/seregaxvm) | [liquidcrystal.lua](../../lua_modules/liquidcrystal/liquidcrystal.lua) [i2c4bit.lua](../../lua_modules/liquidcrystal/lc-i2c4bit.lua) [gpio4bit.lua](../../lua_modules/liquidcrystal/lc-gpio4bit.lua) [gpio8bit.lua](../../lua_modules/liquidcrystal/lc-gpio8bit.lua) |
+
+This Lua module provides access to [Hitachi HD44780](https://www.sparkfun.com/datasheets/LCD/HD44780.pdf) based LCDs. It supports 4 bit and 8 bit GPIO interface, 4 bit [PCF8574](https://www.nxp.com/docs/en/data-sheet/PCF8574_PCF8574A.pdf) based I²C interface.
+
+!!! note
+	This module requires `bit` C module built into firmware. Depending on the interface, `gpio` or `i2c` module is also required.
+
+## Program example
+
+In this example LED screen is connected using I²C GPIO expander.
+Program defines five custom characters and prints text.
+
+```lua
+backend_meta = require "lc-i2c4bit"
+lc_meta = require "liquidcrystal"
+
+-- create display object
+lc = lc_meta(backend_meta{sda=1, scl=2}, false, true, 20)
+backend_meta = nil
+lc_meta = nil
+-- define custom characters
+lc:customChar(0, {0,14,31,31,4,4,5,2})
+lc:customChar(1, {4,6,5,5,4,12,28,8})
+lc:customChar(2, {14,31,17,17,17,17,17,31})
+lc:customChar(3, {14,31,17,17,17,17,31,31})
+lc:customChar(4, {14,31,17,17,31,31,31,31})
+lc:customChar(5, {14,31,31,31,31,31,31,31})
+lc:clear() -- clear display
+lc:blink(true) -- enable cursor blinking
+lc:home() -- reset cursor position
+lc:write("hello", " ", "world") -- write string
+lc:cursorMove(1, 2) -- move cursor to second line
+lc:write("umbrella", 0, 32, "note", 1) -- mix text strings and characters
+lc:cursorMove(1, 3)
+lc:write("Battery level ", 2, 3, 4, 5)
+lc:home()
+lc:blink(false)
+for i=1,20 do print(lc:read()) end -- read back first line
+lc:home()
+for _, d in ipairs(lc:readCustom(0)) do print(d) end -- read back umbrella char
+for _, d in ipairs(lc:readCustom(1)) do print(d) end -- read back note char
+```
+
+### Require
+```lua
+i2c4bit_meta = require("lc-i2c4bit")
+gpio4bit_meta = require("lc-gpio4bit")
+gpio8bit_meta = require("lc-gpio8bit")
+lc_meta = require("liquidcrystal")
+```
+
+### Release
+```lua
+package.loaded["lc-i2c4bit"] = nil
+package.loaded["lc-gpio4bit"] = nil
+package.loaded["lc-gpio8bit"] = nil
+package.loaded["liquidcrystal"] = nil
+```
+
+## Initialization
+
+Liquidcrystal module is initialized using closure, which takes backend object as an argument.
+
+### I²C backend
+
+Loading I²C backend module returns initialization closure.
+It configures I²C backend and returns backend object.
+
+#### Syntax
+`function({[sda=sda_pin] [, scl=scl_pin] [, busid=id] [, busad=address] [, speed = spd] [, rs = rs_pos] [, rw = rw_pos] [, en = en_pos] [, bl = bl_pos] [, d4 = d4_pos] [, d5 = d5_pos] [, d6 = d6_pos] [, d7 = d7_pos]})`
+
+!!! note
+	In most cases only `sda` and `scl` parameters are required
+
+#### Parameters
+- `sda`: I²C data pin. If set to `nil`, I²C bus initialization step via [`i2c.setup`](https://nodemcu.readthedocs.io/en/master/modules/i2c/#i2csetup) will be skipped
+- `scl`: I²C clock pin. If set to `nil`, I²C bus initialization step via [`i2c.setup`](https://nodemcu.readthedocs.io/en/master/modules/i2c/#i2csetup) will be skipped
+- `busid`: I²C bus ID. Defaults to `0`
+- `busad`: chip I²C address. Defaults to `0x27` (default PCF8574 address)
+- `speed`: I²C speed. Defaults to `i2c.SLOW`
+- `rs`: bit position assigned to `RS` pin in I²C word. Defaults to 0
+- `rw`: bit position assigned to `RW` pin in I²C word. Defaults to 1
+- `en`: bit position assigned to `EN` pin in I²C word. Defaults to 2
+- `bl`: bit position assigned to backlight pin in I²C word. Defaults to 3
+- `d4`: bit position assigned to `D4` pin in I²C word. Defaults to 4
+- `d5`: bit position assigned to `D5` pin in I²C word. Defaults to 5
+- `d6`: bit position assigned to `D6` pin in I²C word. Defaults to 6
+- `d7`: bit position assigned to `D7` pin in I²C word. Defaults to 7
+
+#### Returns
+- backend object
+
+#### Example
+```lua
+backend_meta = require "lc-i2c4bit"
+backend = backend_meta{sda=1, scl=2 ,speed=i2c.FAST}
+```
+
+### GPIO 4 bit backend
+
+Loading GPIO 4 bit backend module returns initialization closure.
+It configures GPIO 4 bit backend and returns backend object.
+
+#### Syntax
+`function({[, rs = rs_pos] [, rw = rw_pos] [, en = en_pos] [, bl = bl_pos] [, d4 = d4_pos] [, d5 = d5_pos] [, d6 = d6_pos] [, d7 = d7_pos]})`
+
+#### Parameters
+- `rs`: GPIO pin connected to `RS` pin. Defaults to 0
+- `rw`: GPIO pin connected to `RW` pin. If set to `nil` then `busy`, `position` and `readChar` functions will not be available. Note that `RW` pin must be pulled to the ground if not connected to GPIO
+- `en`: GPIO pin connected to `EN` pin. Defaults to 1
+- `bl`: GPIO pin controlling backlight. It is assumed, that high level turns backlight on, low level turns backlight off. If set to `nil` then backlight function will not be available
+- `d4`: GPIO pin connected to `D4` pin. Defaults to 2
+- `d5`: GPIO pin connected to `D5` pin. Defaults to 3
+- `d6`: GPIO pin connected to `D6` pin. Defaults to 4
+- `d7`: GPIO pin connected to `D7` pin. Defaults to 5
+
+#### Returns
+- backend object
+
+#### Example
+```lua
+backend_meta = require "lc-gpio4bit"
+backend = backend_meta{rs=0, rw=1, en=4, d4=5, d5=6, d6=7, d7=8}
+```
+
+### GPIO 8 bit backend
+
+Loading GPIO 8 bit backend module returns initialization closure.
+It configures GPIO 8 bit backend and returns backend object.
+
+#### Syntax
+`function({[, rs = rs_pos] [, rw = rw_pos] [, en = en_pos] [, bl = bl_pos] [, d0 = d0_pos] [, d1 = d1_pos] [, d2 = d2_pos] [, d3 = d3_pos] [, d4 = d4_pos] [, d5 = d5_pos] [, d6 = d6_pos] [, d7 = d7_pos]})`
+
+#### Parameters
+- `rs`: GPIO pin connected to `RS` pin. Defaults to 0
+- `rw`: GPIO pin connected to `RW` pin. If set to `nil` then `busy`, `position` and `readChar` functions will not be available. Note that `RW` pin must be pulled to the ground if not connected to GPIO
+- `en`: GPIO pin connected to `EN` pin. Defaults to 1
+- `bl`: GPIO pin controlling backlight. It is assumed, that high level turns backlight on, low level turns backlight off. If set to `nil` then backlight function will not be available
+- `d0`: GPIO pin connected to `D0` pin. Defaults to 2
+- `d1`: GPIO pin connected to `D1` pin. Defaults to 3
+- `d2`: GPIO pin connected to `D2` pin. Defaults to 4
+- `d3`: GPIO pin connected to `D3` pin. Defaults to 5
+- `d4`: GPIO pin connected to `D4` pin. Defaults to 6
+- `d5`: GPIO pin connected to `D5` pin. Defaults to 7
+- `d6`: GPIO pin connected to `D6` pin. Defaults to 8
+- `d7`: GPIO pin connected to `D7` pin. Defaults to 9
+
+#### Returns
+- backend object
+
+#### Example
+```lua
+backend_meta = require "lc-gpio8bit"
+backend = backend_meta{rs=15, rw=2, en=5, d0=23, d1=13, d2=33, d3=32, d4=18, d5=19, d6=21, d7=22}
+```
+
+### Liquidcrystal initialization
+
+Loading Liquidcrystal module returns initialization closure.
+It requires backend object and returns LCD object.
+
+#### Syntax
+`function(backend, onelinemode, eightdotsmode, column_width)`
+
+#### Parameters
+- `backend`: backend object
+- `onelinemode`: `true` to use one line mode, `false` to use two line mode
+- `eightdotsmode`: `true` to use 5x8 dot font, `false` to use 5x10 dot font
+- `column_width`: number of characters in column. Used for offset calculations in function `cursorMove`. If set to `nil`, functionality of `cursorMove` will be limited. For most displays column width is `20` characters
+
+#### Returns
+screen object
+
+#### Example
+```lua
+lc_meta = require "liquidcrystal"
+lc = lc_meta(backend, true, true, 20)
+```
+
+## liquidcrystal.autoscroll
+Autoscroll text when printing. When turned off, cursor moves and text stays still, when turned on, vice versa.
+
+#### Syntax
+`liquidcrystal.autoscroll(self, on)`
+
+#### Parameters
+- `self`: `liquidcrystal` instance
+- `on`: `true` to turn on, `false` to turn off
+
+#### Returns
+- sent data
+
+#### Example
+```lua
+liquidcrystal:autoscroll(true)
+```
+
+## liquidcrystal.backlight
+Control LCDs backlight. When using GPIO backend without `bl` argument specification function does nothing.
+
+#### Syntax
+`liquidcrystal.backlight(self, on)`
+
+#### Parameters
+- `self`: `liquidcrystal` instance
+- `on`: `true` to turn on, `false` to turn off
+
+#### Returns
+- backlight status
+
+#### Example
+```lua
+liquidcrystal:backlight(true)
+```
+
+## liquidcrystal.blink
+Control cursors blink mode.
+
+#### Syntax
+`liquidcrystal.blink(self, on)`
+
+#### Parameters
+- `self`: `liquidcrystal` instance
+- `on`: `true` to turn on, `false` to turn off
+
+#### Returns
+- sent data
+
+#### Example
+```lua
+liquidcrystal:blink(true)
+```
+
+## liquidcrystal.busy
+Get busy status of the LCD. When using GPIO backend without `rw` argument specification function does nothing.
+
+#### Syntax
+`liquidcrystal.busy(self)`
+
+#### Parameters
+- `self`: `liquidcrystal` instance
+
+#### Returns
+- `true` if device is busy, `false` if device is ready to receive commands
+
+#### Example
+```lua
+while liquidcrystal:busy() do end
+```
+
+## liquidcrystal.clear
+Clear LCD screen.
+
+#### Syntax
+`liquidcrystal.clear(self)`
+
+#### Parameters
+- `self`: `liquidcrystal` instance
+
+#### Returns
+- sent data
+
+#### Example
+```lua
+liquidcrystal:clear()
+```
+
+## liquidcrystal.cursorLeft
+Move cursor one character to the left.
+
+#### Syntax
+`liquidcrystal.cursorLeft(self)`
+
+#### Parameters
+- `self`: `liquidcrystal` instance
+
+#### Returns
+- sent data
+
+#### Example
+```lua
+liquidcrystal:cursorLeft()
+```
+
+## liquidcrystal.cursorMove
+Move cursor to position. If `row` not specified, move cursor to address `col`.
+
+!!! note
+	Note that column and row indexes start with 1. However, when omitting `row` parameter, cursor addresses start with 0.
+
+#### Syntax
+`liquidcrystal.cursorMove(self, col, row)`
+
+#### Parameters
+- `self`: `liquidcrystal` instance
+- `col`: new cursor position column. If `row` not specified, new cursor position address
+- `row`: new cursor position row or `nil`
+
+#### Returns
+- sent data
+
+#### Example
+```lua
+liquidcrystal:cursorMove(5, 1)
+liquidcrystal:cursorMove(10, 4)
+liquidcrystal:cursorMove(21)
+```
+
+## liquidcrystal.cursor
+Control cursors highlight mode.
+
+#### Syntax
+`liquidcrystal.cursor(self, on)`
+
+#### Parameters
+- `self`: `liquidcrystal` instance
+- `on`: `true` to turn on, `false` to turn off
+
+#### Returns
+- sent data
+
+#### Example
+```lua
+liquidcrystal:cursor(true)
+```
+
+## liquidcrystal.cursorRight
+Move cursor one character to the right.
+
+#### Syntax
+`liquidcrystal.cursorRight(self)`
+
+#### Parameters
+- `self`: `liquidcrystal` instance
+
+#### Returns
+- sent data
+
+#### Example
+```lua
+liquidcrystal:cursorRight()
+```
+
+## liquidcrystal.customChar
+Define new custom char. Up to 8 custom characters with indexes 0 to 7 may be defined in eight dot mode.
+They are accessed via `write` function by index.
+In ten dot mode only 4 custom characters may be used.
+They are numbered from 0 to 7 with half of them being aliases to each other (0 to 1, 2 to 3 etc).
+
+!!! note
+	Upon redefinition of a custom character all its instances will be updated automatically.
+	
+	This function resets cursor position to home if `liquidcrystal.position` function is not available.
+	
+	There are web services ([1](https://omerk.github.io/lcdchargen/), [2](https://www.quinapalus.com/hd44780udg.html)) and [desktop applications](https://pypi.org/project/lcdchargen/) that help create custom characters.
+
+#### Syntax
+`liquidcrystal.customChar(self, index, bytes)`
+
+#### Parameters
+- `self`: `liquidcrystal` instance
+- `index`: custom char index in range from 0 to 7
+- `bytes`: array of 8 bytes in eight bit mode or 11 bytes in ten bit mode (eleventh line is a cursor line that can also be used) that defines new char bitmap line by line
+
+#### Returns
+`nil`
+
+#### Example
+```lua
+liquidcrystal:customChar(5, {14,31,31,31,31,31,31,31})
+liquidcrystal:write(5)
+```
+
+## liquidcrystal.display
+Turn display on and off. Does not affect display backlight. Does not clear the display.
+
+#### Syntax
+`liquidcrystal.display(self, on)`
+
+#### Parameters
+- `self`: `liquidcrystal` instance
+- `on`: `true` to turn on, `false` to turn off
+
+#### Returns
+- sent data
+
+#### Example
+```lua
+liquidcrystal:display(true)
+```
+
+## liquidcrystal.home
+Reset cursor and screen position.
+
+#### Syntax
+`liquidcrystal.home(self)`
+
+#### Parameters
+- `self`: `liquidcrystal` instance
+
+#### Returns
+- sent data
+
+#### Example
+```lua
+liquidcrystal:home()
+```
+
+## liquidcrystal.leftToRight
+Print text left to right (default).
+
+#### Syntax
+`liquidcrystal.leftToRight(self)`
+
+#### Parameters
+- `self`: `liquidcrystal` instance
+
+#### Returns
+- sent data
+
+#### Example
+```lua
+liquidcrystal:leftToRight()
+```
+
+## liquidcrystal.position
+Get current position of the cursor. Position is 0 indexed. When using GPIO backend without `rw` argument specification function does nothing.
+
+#### Syntax
+`liquidcrystal.position(self)`
+
+#### Parameters
+- `self`: `liquidcrystal` instance
+
+#### Returns
+- 0 indexed position of the cursor
+
+#### Example
+```lua
+local pos = liquidcrystal:position() -- save position
+-- some code
+liquidcrystal:cursorMove(pos) -- restore position
+```
+
+## liquidcrystal.read
+Return current character numerical representation.
+When using GPIO backend without `rw` argument specification function does nothing.
+
+#### Syntax
+`liquidcrystal.read(self)`
+
+#### Parameters
+- `self`: `liquidcrystal` instance
+
+#### Returns
+- numerical representation of the current character
+
+#### Example
+```lua
+liquidcrystal:home() -- goto home
+local ch = liquidcrystal:read() -- read char
+liquidcrystal:cursorMove(1, 2) -- move to the second line
+for i=ch,ch+5 do lc:write(i) end -- print 6 chars starting with ch
+```
+
+## liquidcrystal.readCustom
+Return custom char byte array.
+When using GPIO backend without `rw` argument specification function returns zeros.
+
+#### Syntax
+`liquidcrystal.readCustom(self, index)`
+
+#### Parameters
+- `self`: `liquidcrystal` instance
+- `index`: custom char index in range from 0 to 7
+
+#### Returns
+- table of size 8 in eight dot mode or 11 in ten dot mode. Each 8 bit number represents a character dot line
+
+#### Example
+```lua
+lc:customChar(0, {0,14,31,31,4,4,5,2}) -- define custom character
+for _, d in ipairs(lc:readCustom(0)) do print(d) end -- read it back
+```
+
+## liquidcrystal.rightToLeft
+Print text right to left.
+
+#### Syntax
+`liquidcrystal.rightToLeft(self)`
+
+#### Parameters
+- `self`: `liquidcrystal` instance
+
+#### Returns
+- sent data
+
+#### Example
+```lua
+liquidcrystal:rightToLeft()
+```
+
+## liquidcrystal.scrollLeft
+Move text to the left.
+
+#### Syntax
+`liquidcrystal.scrollLeft(self)`
+
+#### Parameters
+- `self`: `liquidcrystal` instance
+
+#### Returns
+- sent data
+
+#### Example
+```lua
+liquidcrystal:scrollLeft()
+```
+
+## liquidcrystal.scrollRight
+Move text to the right.
+
+#### Syntax
+`liquidcrystal.scrollRight(self)`
+
+#### Parameters
+- `self`: `liquidcrystal` instance
+
+#### Returns
+- sent data
+
+#### Example
+```lua
+liquidcrystal:scrollRight()
+```
+
+## liquidcrystal.write
+Print text.
+
+#### Syntax
+`liquidcrystal.write(self, ...)`
+
+#### Parameters
+- `self`: `liquidcrystal` instance
+- `...`: strings or char codes. For the list of available characters refer to [HD44780 datasheet](https://www.sparkfun.com/datasheets/LCD/HD44780.pdf#page=17)
+
+#### Returns
+`nil`
+
+#### Example
+```lua
+liquidcrystal:write("hello world")
+liquidcrystal:write("hello yourself", "!!!", 243, 244)
+```
+
+

+ 83 - 0
lua_modules/liquidcrystal/lc-gpio4bit.lua

@@ -0,0 +1,83 @@
+local gpio, bit = gpio, bit
+
+return function(bus_args)
+   local rs = bus_args.rs or 0
+   local rw = bus_args.rw
+   local en = bus_args.en or 1
+   local bl = bus_args.backlight
+   local d4 = bus_args.d4 or 2
+   local d5 = bus_args.d5 or 3
+   local d6 = bus_args.d6 or 4
+   local d7 = bus_args.d7 or 5
+
+   for _, d in pairs({rs,rw,en,bl}) do
+      if d then
+         gpio.mode(d, gpio.OUTPUT)
+      end
+   end
+
+   local function setGPIO(mode)
+      for _, d in pairs({d4, d5, d6, d7}) do
+         gpio.mode(d, mode)
+      end
+   end
+
+   setGPIO(gpio.OUTPUT)
+
+   local function send4bitGPIO(value, rs_en, rw_en, read)
+      local function exchange(data)
+         local rv = 0
+         if rs then gpio.write(rs, rs_en and gpio.HIGH or gpio.LOW) end
+         if rw then gpio.write(rw, rw_en and gpio.HIGH or gpio.LOW) end
+         gpio.write(en, gpio.HIGH)
+         for i, d in ipairs({d4, d5, d6, d7}) do
+            if read and rw then
+               if gpio.read(d) == 1 then rv = bit.set(rv, i-1) end
+            else
+               gpio.write(d, bit.isset(data, i-1) and gpio.HIGH or gpio.LOW)
+            end
+         end
+         gpio.write(en, gpio.LOW)
+         return rv
+      end
+      local hi = bit.rshift(bit.band(value, 0xf0), 4)
+      local lo = bit.band(value, 0xf)
+      if read then setGPIO(gpio.INPUT) end
+      hi = exchange(hi)
+      lo = exchange(lo)
+      if read then setGPIO(gpio.OUTPUT) end
+      return bit.bor(bit.lshift(hi, 4), lo)
+   end
+
+   -- init sequence from datasheet
+   send4bitGPIO(0x33, false, false, false)
+   send4bitGPIO(0x32, false, false, false)
+
+   -- Return backend object
+   return {
+      fourbits  = true,
+      command   = function (screen, cmd)
+         return send4bitGPIO(cmd, false, false, false)
+      end,
+      busy      = function(screen)
+         if rw == nil then return false end
+         return bit.isset(send4bitGPIO(0xff, false, true, true), 7)
+      end,
+      position  = function(screen)
+         if rw == nil then return 0 end
+         return bit.clear(send4bitGPIO(0xff, false, true, true), 7)
+      end,
+      write     = function(screen, value)
+         return send4bitGPIO(value, true, false, false)
+      end,
+      read      = function(screen)
+         if rw == nil then return nil end
+         return send4bitGPIO(0xff, true, true, true)
+      end,
+      backlight = function(screen, on)
+         if (bl) then gpio.write(bl, on and gpio.HIGH or gpio.LOW) end
+         return on
+      end,
+   }
+
+end

+ 80 - 0
lua_modules/liquidcrystal/lc-gpio8bit.lua

@@ -0,0 +1,80 @@
+local gpio, bit = gpio, bit
+
+return function(bus_args)
+   local rs = bus_args.rs or 0
+   local rw = bus_args.rw
+   local en = bus_args.en or 1
+   local bl = bus_args.backlight
+   local d0 = bus_args.d0 or 2
+   local d1 = bus_args.d1 or 3
+   local d2 = bus_args.d2 or 4
+   local d3 = bus_args.d3 or 5
+   local d4 = bus_args.d4 or 6
+   local d5 = bus_args.d5 or 7
+   local d6 = bus_args.d6 or 8
+   local d7 = bus_args.d7 or 9
+
+   for _, d in pairs({rs,rw,en,bl}) do
+      if d then
+         gpio.mode(d, gpio.OUTPUT)
+      end
+   end
+
+   local function setGPIO(mode)
+      for _, d in pairs({d0, d1, d2, d3, d4, d5, d6, d7}) do
+         gpio.mode(d, mode)
+      end
+   end
+
+   setGPIO(gpio.OUTPUT)
+
+   local function send8bitGPIO(value, rs_en, rw_en, read)
+      local function exchange(data)
+         local rv = 0
+         if rs then gpio.write(rs, rs_en and gpio.HIGH or gpio.LOW) end
+         if rw then gpio.write(rw, rw_en and gpio.HIGH or gpio.LOW) end
+         gpio.write(en, gpio.HIGH)
+         for i, d in ipairs({d0, d1, d2, d3, d4, d5, d6, d7}) do
+            if read and rw then
+               if gpio.read(d) == 1 then rv = bit.set(rv, i-1) end
+            else
+               gpio.write(d, bit.isset(data, i-1) and gpio.HIGH or gpio.LOW)
+            end
+         end
+         gpio.write(en, gpio.LOW)
+         return rv
+      end
+      if read then setGPIO(gpio.INPUT) end
+      value = exchange(value)
+      if read then setGPIO(gpio.OUTPUT) end
+      return value
+   end
+
+   -- Return backend object
+   return {
+      fourbits  = false,
+      command   = function (screen, cmd)
+         return send8bitGPIO(cmd, false, false, false)
+      end,
+      busy      = function(screen)
+         if rw == nil then return false end
+         return bit.isset(send8bitGPIO(0xff, false, true, true), 7)
+      end,
+      position  = function(screen)
+         if rw == nil then return 0 end
+         return bit.clear(send8bitGPIO(0xff, false, true, true), 7)
+      end,
+      write     = function(screen, value)
+         return send8bitGPIO(value, true, false, false)
+      end,
+      read      = function(screen)
+         if rw == nil then return nil end
+         return send8bitGPIO(0xff, true, true, true)
+      end,
+      backlight = function(screen, on)
+         if (bl) then gpio.write(bl, on and gpio.HIGH or gpio.LOW) end
+         return on
+      end,
+   }
+
+end

+ 103 - 0
lua_modules/liquidcrystal/lc-i2c4bit.lua

@@ -0,0 +1,103 @@
+local i2c, bit = i2c, bit
+
+return function(bus_args)
+   local busid = bus_args.id or 0
+   local busad = bus_args.address or 0x27
+   local speed = bus_args.speed or i2c.SLOW
+
+   local rs = bus_args.rs or 0
+   local rw = bus_args.rw or 1
+   local en = bus_args.en or 2
+   local bl = bus_args.backlight or 3
+   local d4 = bus_args.d4 or 4
+   local d5 = bus_args.d5 or 5
+   local d6 = bus_args.d6 or 6
+   local d7 = bus_args.d7 or 7
+
+   -- Convenience I2C setup if a pin configuration is given
+   if bus_args.sda ~= nil and bus_args.scl ~= nil then
+      i2c.setup(busid, bus_args.sda, bus_args.scl, speed)
+   end
+
+   -- The onus is on us to maintain the backlight state
+   local backlight = true
+
+   local function send4bitI2C(value, rs_en, rw_en, read)
+      local function exchange(data, unset_read)
+         local rv = data
+         i2c.start(busid)
+         i2c.address(busid, busad, i2c.TRANSMITTER)
+         i2c.write(busid, bit.set(data, en))
+         if read then
+            i2c.start(busid)
+            i2c.address(busid, busad, i2c.RECEIVER)
+            rv = i2c.read(busid, 1):byte(1)
+            i2c.start(busid)
+            i2c.address(busid, busad, i2c.TRANSMITTER)
+            if unset_read then data = bit.bor(bit.bit(rs),
+                                              bit.bit(rw),
+                                              backlight and bit.bit(bl) or 0) end
+	    i2c.write(busid, bit.set(data, en))
+         end
+         i2c.write(busid, bit.clear(data, en))
+         i2c.stop(busid)
+         return rv
+      end
+      local lo = bit.bor(bit.isset(value, 0) and bit.bit(d4) or 0,
+                         bit.isset(value, 1) and bit.bit(d5) or 0,
+                         bit.isset(value, 2) and bit.bit(d6) or 0,
+                         bit.isset(value, 3) and bit.bit(d7) or 0)
+      local hi = bit.bor(bit.isset(value, 4) and bit.bit(d4) or 0,
+                         bit.isset(value, 5) and bit.bit(d5) or 0,
+                         bit.isset(value, 6) and bit.bit(d6) or 0,
+                         bit.isset(value, 7) and bit.bit(d7) or 0)
+      local cmd = bit.bor(rs_en and bit.bit(rs) or 0,
+                          rw_en and bit.bit(rw) or 0,
+                          backlight and bit.bit(bl) or 0)
+      hi = exchange(bit.bor(cmd, hi), false)
+      lo = exchange(bit.bor(cmd, lo), true)
+      return bit.bor(bit.lshift(bit.isset(lo, d4) and 1 or 0, 0),
+                     bit.lshift(bit.isset(lo, d5) and 1 or 0, 1),
+                     bit.lshift(bit.isset(lo, d6) and 1 or 0, 2),
+                     bit.lshift(bit.isset(lo, d7) and 1 or 0, 3),
+                     bit.lshift(bit.isset(hi, d4) and 1 or 0, 4),
+                     bit.lshift(bit.isset(hi, d5) and 1 or 0, 5),
+                     bit.lshift(bit.isset(hi, d6) and 1 or 0, 6),
+                     bit.lshift(bit.isset(hi, d7) and 1 or 0, 7))
+   end
+
+   -- init sequence from datasheet
+   send4bitI2C(0x33, false, false, false)
+   send4bitI2C(0x32, false, false, false)
+
+   -- Return backend object
+   return {
+      fourbits  = true,
+      command   = function (screen, cmd)
+         return send4bitI2C(cmd, false, false, false)
+      end,
+      busy      = function(screen)
+         local rv = send4bitI2C(0xff, false, true, true)
+         send4bitI2C(bit.bor(0x80, bit.clear(rv, 7)), false, false, false)
+         return bit.isset(rv, 7)
+      end,
+      position  = function(screen)
+         local rv = bit.clear(send4bitI2C(0xff, false, true, true), 7)
+         send4bitI2C(bit.bor(0x80, rv), false, false, false)
+         return rv
+      end,
+      write     = function(screen, value)
+         return send4bitI2C(value, true, false, false)
+      end,
+      read      = function(screen)
+         return send4bitI2C(0xff, true, true, true)
+      end,
+      backlight = function(screen, on)
+         backlight = on
+         local rv = bit.clear(send4bitI2C(0xff, false, true, true), 7)
+         send4bitI2C(bit.bor(0x80, rv), false, false, false)
+         return on
+      end,
+   }
+
+end

+ 183 - 0
lua_modules/liquidcrystal/liquidcrystal.lua

@@ -0,0 +1,183 @@
+local bit = bit
+-- metatable
+local LiquidCrystal = {}
+LiquidCrystal.__index = LiquidCrystal
+
+-- commands
+local LCD_CLEARDISPLAY = 0x01
+local LCD_RETURNHOME = 0x02
+local LCD_ENTRYMODESET = 0x04
+local LCD_DISPLAYCONTROL = 0x08
+local LCD_CURSORSHIFT = 0x10
+local LCD_FUNCTIONSET = 0x20
+local LCD_SETCGRAMADDR = 0x40
+local LCD_SETDDRAMADDR = 0x80
+
+-- flags for display entry mode
+-- local LCD_ENTRYRIGHT = 0x00
+local LCD_ENTRYLEFT = 0x02
+local LCD_ENTRYSHIFTINCREMENT = 0x01
+-- local LCD_ENTRYSHIFTDECREMENT = 0x00
+
+-- flags for display on/off control
+local LCD_DISPLAYON = 0x04
+-- local LCD_DISPLAYOFF = 0x00
+local LCD_CURSORON = 0x02
+-- local LCD_CURSOROFF = 0x00
+local LCD_BLINKON = 0x01
+-- local LCD_BLINKOFF = 0x00
+
+-- flags for display/cursor shift
+local LCD_DISPLAYMOVE = 0x08
+local LCD_CURSORMOVE = 0x00
+local LCD_MOVERIGHT = 0x04
+local LCD_MOVELEFT = 0x00
+
+-- flags for function set
+local LCD_8BITMODE = 0x10
+local LCD_4BITMODE = 0x00
+local LCD_2LINE = 0x08
+local LCD_1LINE = 0x00
+local LCD_5x10DOTS = 0x04
+local LCD_5x8DOTS = 0x00
+
+
+function LiquidCrystal:autoscroll(on)
+   if on then
+      self._displaymode = bit.bor(self._displaymode, LCD_ENTRYSHIFTINCREMENT)
+   else
+      self._displaymode = bit.band(self._displaymode, bit.bnot(LCD_ENTRYSHIFTINCREMENT))
+   end
+   return self:_command(bit.bor(LCD_ENTRYMODESET, self._displaymode))
+end
+
+function LiquidCrystal:blink(on)
+   if on then
+      self._displaycontrol = bit.bor(self._displaycontrol, LCD_BLINKON)
+   else
+      self._displaycontrol = bit.band(self._displaycontrol, bit.bnot(LCD_BLINKON))
+   end
+   return self:_command(bit.bor(LCD_DISPLAYCONTROL, self._displaycontrol))
+end
+
+function LiquidCrystal:clear() return self:_command(LCD_CLEARDISPLAY) end
+
+function LiquidCrystal:cursorLeft()
+   return self:_command(bit.bor(LCD_CURSORSHIFT, LCD_CURSORMOVE, LCD_MOVELEFT))
+end
+
+function LiquidCrystal:cursorMove(col, row)
+   return self:_command(bit.bor(LCD_SETDDRAMADDR, col + (row and (self._offsets[row] - 1) or 0)))
+end
+
+function LiquidCrystal:cursor(on)
+   if on then
+      self._displaycontrol = bit.bor(self._displaycontrol, LCD_CURSORON)
+   else
+      self._displaycontrol = bit.band(self._displaycontrol, bit.bnot(LCD_CURSORON))
+   end
+   return self:_command(bit.bor(LCD_DISPLAYCONTROL, self._displaycontrol))
+end
+
+function LiquidCrystal:cursorRight()
+   return self:_command(bit.bor(LCD_CURSORSHIFT, LCD_CURSORMOVE, LCD_MOVERIGHT))
+end
+
+function LiquidCrystal:customChar(index, bytes)
+   local pos = self:position()
+   self:_command(bit.bor(LCD_SETCGRAMADDR,
+                         bit.lshift(bit.band(self._eightdots and index or bit.clear(index, 0),
+                                             0x7), 3)))
+   for i=1,(self._eightdots and 8 or 11) do self:_write(bytes[i] or 0) end
+   self:cursorMove(pos)
+end
+
+function LiquidCrystal:display(on)
+   if on then
+      self._displaycontrol = bit.bor(self._displaycontrol, LCD_DISPLAYON)
+   else
+      self._displaycontrol = bit.band(self._displaycontrol, bit.bnot(LCD_DISPLAYON))
+   end
+   return self:_command(bit.bor(LCD_DISPLAYCONTROL, self._displaycontrol))
+end
+
+function LiquidCrystal:home() return self:_command(LCD_RETURNHOME) end
+
+function LiquidCrystal:leftToRight()
+   self._displaymode = bit.bor(self._displaymode, LCD_ENTRYLEFT)
+   return self:_command(bit.bor(LCD_ENTRYMODESET, self._displaymode))
+end
+
+function LiquidCrystal:readCustom(index)
+   local pos = self:position()
+   local data = {}
+   self:_command(bit.bor(LCD_SETCGRAMADDR,
+                         bit.lshift(bit.band(self._eightdots and index or bit.clear(index, 0),
+                                             0x7), 3)))
+   for i=1,(self._eightdots and 8 or 11) do data[i] = self:read() end
+   self:cursorMove(pos)
+   return data
+end
+
+function LiquidCrystal:rightToLeft()
+   self._displaymode = bit.band(self._displaymode, bit.bnot(LCD_ENTRYLEFT))
+   self:_command(bit.bor(LCD_ENTRYMODESET, self._displaymode))
+end
+
+function LiquidCrystal:scrollLeft()
+   return self:_command(bit.bor(LCD_CURSORSHIFT, LCD_DISPLAYMOVE, LCD_MOVELEFT))
+end
+
+function LiquidCrystal:scrollRight()
+   return self:_command(bit.bor(LCD_CURSORSHIFT, LCD_DISPLAYMOVE, LCD_MOVERIGHT))
+end
+
+function LiquidCrystal:write(...)
+   for _, x in ipairs({...}) do
+      if type(x) == "number" then
+	 self:_write(x)
+      end
+      if type(x) == "string" then
+	 for i=1,#x do
+	    self:_write(string.byte(x, i))
+	 end
+      end
+   end
+end
+
+
+return function (backend, onelinemode, eightdotsmode, column_width)
+   local self = {}
+   setmetatable(self, LiquidCrystal)
+
+   -- copy out backend functions, to avoid a long-lived table
+   self._command  = backend.command
+   self.busy      = backend.busy
+   self.position  = backend.position
+   self._write    = backend.write
+   self.read      = backend.read
+   self.backlight = backend.backlight
+
+   -- defaults
+   self._displaycontrol = 0
+   self._displaymode = 0
+
+   self._eightdots = eightdotsmode
+   self._offsets = {0, 0x40}
+   if column_width ~= nil then
+      self._offsets[3] =  0    + column_width
+      self._offsets[4] =  0x40 + column_width
+   end
+
+   self:_command(bit.bor(LCD_FUNCTIONSET,
+                         bit.bor(
+                            backend.fourbits and LCD_4BITMODE or LCD_8BITMODE,
+                            onelinemode      and LCD_1LINE or LCD_2LINE,
+                            eightdotsmode    and LCD_5x8DOTS or LCD_5x10DOTS)))
+   self:_command(bit.bor(LCD_ENTRYMODESET, self._displaymode))
+
+   self:display(true)
+   self:clear()
+
+   return self
+end

+ 1 - 0
mkdocs.yml

@@ -54,6 +54,7 @@ pages:
     - 'hdc1000': 'lua-modules/hdc1000.md'
     - 'httpserver': 'lua-modules/httpserver.md'
     - 'imap': 'lua-modules/imap.md'
+    - 'liquidcrystal': 'lua-modules/liquidcrystal.md'
     - 'lm92': 'lua-modules/lm92.md'
     - 'mcp23008': 'lua-modules/mcp23008.md'
     - 'redis': 'lua-modules/redis.md'