diff --git a/kitchentext b/kitchentext
new file mode 100755
index 0000000..f60f7af
--- /dev/null
+++ b/kitchentext
@@ -0,0 +1,184 @@
+#!/usr/bin/env python3
+#
+# kitchentext: Read text from stdin and put it on the Kitchenlight.
+#
+# Author: Shy
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see .
+
+def kitchentext(delay=200, wait=False, restore=False, poweron=False,
+ verbose=False, debug=False):
+
+ import sys, signal
+ from c4ctrl import C4Interface, Kitchenlight
+
+ charwidth = { # Width of characters.
+ 'a' : 5, 'A' : 5, 'b' : 4, 'B' : 5, 'c' : 3, 'C' : 5,
+ 'd' : 4, 'D' : 5, 'e' : 4, 'E' : 5, 'f' : 3, 'F' : 5,
+ 'g' : 3, 'G' : 5, 'h' : 3, 'H' : 5, 'i' : 1, 'I' : 5,
+ 'j' : 2, 'J' : 5, 'k' : 3, 'K' : 5, 'l' : 3, 'L' : 5,
+ 'm' : 5, 'M' : 5, 'n' : 4, 'N' : 5, 'o' : 3, 'O' : 5,
+ 'p' : 3, 'P' : 5, 'q' : 3, 'Q' : 5, 'r' : 3, 'R' : 5,
+ 's' : 4, 'S' : 5, 't' : 3, 'T' : 5, 'u' : 3, 'U' : 5,
+ 'v' : 3, 'V' : 5, 'w' : 5, 'W' : 5, 'x' : 3, 'X' : 5,
+ 'y' : 3, 'Y' : 5, 'z' : 3, 'Z' : 5, '0' : 5, '1' : 4,
+ '2' : 5, '3' : 5, '4' : 5, '5' : 5, '6' : 5, '7' : 5,
+ '8' : 5, '9' : 5, '@' : 5, '=' : 3, '!' : 1, '"' : 3,
+ '_' : 5, '-' : 3, '.' : 2, ',' : 2, '*' : 5, ':' : 2,
+ '\'' : 1, '/' : 5, '(' : 2, ')' : 2, '{' : 3, '}' : 3,
+ '[' : 2, ']' : 2, '<' : 3, '>' : 4, '+' : 5, '#' : 5,
+ '$' : 5, '%' : 5, '$' : 5, '~' : 5, '?' : 3, ';' : 2,
+ '\\' : 5, '^' : 3, '|' : 1, '`' : 2, ' ' : 3, '\t' : 5
+ }
+
+ C4Interface.debug = debug
+ kl = Kitchenlight(autopower=poweron)
+
+ # Enforce wait=True if restore=True
+ wait = wait or restore
+
+ # Store previous state.
+ if restore:
+ c4 = C4Interface()
+ saved_state = c4.pull([kl.topic, kl.powertopic])
+
+ # We want to be able to restore the saved Kitchenlight if we receive a
+ # SIGERM signal. We do this by creating an registering a custom exception
+ # class.
+ class KitchenSignalError(Exception):
+ def __init__(self, signal, frame):
+ self.signal = signal
+ self.frame = frame
+
+ def signal_handler(signal, frame):
+ raise KitchenSignalError(signal, frame)
+
+ try: signal.signal(signal.SIGTERM, signal_handler)
+ except: pass
+
+ try:
+ while True:
+
+ try:
+ text = sys.stdin.readline()
+ except KeyboardInterrupt: # Exit a bit more graceful on CTRL-C.
+ verbose and print("\nInterrupted by user.", file=sys.stderr)
+ sys.exit(1)
+
+ if text == "\n": # Empty line.
+ verbose and print("Info: skipping empty line")
+ continue
+ elif text == "": # EOF.
+ break
+
+ # Strip chars Kitchenlight can not display.
+ text = text.rstrip('\n').encode("ascii", "ignore").decode("ascii")
+
+ try: # We might well get interrupted while waiting.
+ kl.text(text, delay)
+
+ if wait:
+ from time import sleep
+ # How long shall we wait? Kitchenlight has 30 columns, each
+ # char uses 2-5 columns followed by an empty one. The
+ # traversal of one column takes 30 * ms.
+ text_width = 0
+ for c in text:
+ try:
+ text_width += (charwidth[c] + 1)
+ except KeyError:
+ # No width specified for this charachter. Let's use
+ # 4 as default.
+ text_width += 4
+
+ waiting_time = ((30 * delay) + (text_width * delay)) / 1000
+ verbose and print("Waiting for {} seconds ...".format(waiting_time))
+ sleep(waiting_time)
+
+ except KeyboardInterrupt:
+ verbose and print("\nInterrupted by user.", file=sys.stderr)
+ sys.exit(1)
+
+ except KitchenSignalError as error:
+ verbose and print("\nInterrupted by signal {}".format(error.signal),
+ file=sys.stderr)
+ sys.exit(1)
+
+ finally:
+ # Always restore privious state if "restore" is set.
+ if restore:
+ re = []
+ for top in saved_state: re.append((top.topic, top.payload))
+ c4.push(re)
+
+
+if __name__ == "__main__":
+ import argparse
+
+ parser = argparse.ArgumentParser(
+ description="Read multiple lines of text from stdin and put them on \
+ the Kitchenlight.")
+ parser.add_argument(
+ "--debug", action="store_true",
+ help="display what would be send to the MQTT broker, but do not \
+ actually connect")
+ parser.add_argument(
+ "-d", "--delay", type=int, default=200,
+ help="delay in ms (speed of the text, default is 200)")
+ parser.add_argument(
+ "-w", "--wait", action="store_true", default=False,
+ help="wait until the text has been displayed")
+ parser.add_argument(
+ "-r", "--restore", action="store_true", default=False,
+ help="restore the Kitchenlight to its prior state after the text has \
+ been displayed (implies --wait)")
+ parser.add_argument(
+ "-p", "--power-on", action="store_true", default=False,
+ help="turn on Kitchenlight if it is powered off")
+ parser.add_argument(
+ "-f", "--fork", action="store_true",
+ help="fork to background")
+ parser.add_argument(
+ "-F", action="store_true",
+ help="like '-f', but print PID of forked process to stdout")
+ parser.add_argument(
+ "-v", "--verbose", action="store_true",
+ help="be more verbose")
+ args = parser.parse_args()
+
+ if args.fork or args.F:
+ import sys
+ from os import fork
+
+ if sys.stdin.isatty():
+ print("Error: cannot fork when stdin is connected to a terminal!",
+ file=sys.stderr)
+ sys.exit(1)
+
+ child_pid = fork()
+ if child_pid != 0:
+ if args.F:
+ print(child_pid)
+ elif args.verbose:
+ print("Forked to PID", child_pid)
+
+ sys.exit()
+
+ kitchentext(delay=args.delay,
+ wait=args.wait,
+ restore=args.restore,
+ poweron=args.power_on,
+ verbose=args.verbose,
+ debug=args.debug)
+