subDesTagesMitExtraKaese пре 3 година
комит
3177ef0b7a
12 измењених фајлова са 2444 додато и 0 уклоњено
  1. 3 0
      .gitignore
  2. 25 0
      Makefile
  3. 783 0
      armbianio.c
  4. 185 0
      armbianio.h
  5. 68 0
      helper.h
  6. 424 0
      main.c
  7. 329 0
      main.py
  8. 470 0
      max7219.c
  9. 73 0
      max7219.h
  10. 6 0
      run.sh
  11. 36 0
      types.h
  12. 42 0
      webcam.py

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+*.o
+*.a
+build

+ 25 - 0
Makefile

@@ -0,0 +1,25 @@
+  
+CFLAGS=-c -Wall -O2
+LIBS = -lm -lpthread -larmbianio -lpaho-mqtt3c
+CFLAGS += $(shell pkg-config --cflags json-c)
+LIBS += $(shell pkg-config --libs json-c)
+
+all: libarmbianio.a max7219.o main.o
+	$(CC) main.o max7219.o $(LIBS) -o build
+
+main.o: main.c types.h helper.h
+	$(CC) $(CFLAGS) main.c
+
+max7219.o: max7219.c
+	$(CC) $(CFLAGS) max7219.c
+
+libarmbianio.a: armbianio.o
+	ar -rc libarmbianio.a armbianio.o ;\
+	sudo cp libarmbianio.a /usr/local/lib ;\
+	sudo cp armbianio.h /usr/local/include
+
+armbianio.o: armbianio.c
+	$(CC) $(CFLAGS) armbianio.c
+
+clean:
+	rm *.o *.a build

+ 783 - 0
armbianio.c

@@ -0,0 +1,783 @@
+//
+// Armbian IO library
+//
+// Copyright (c) 2017 BitBank Software, Inc.
+// written by Larry Bank
+// email: bitbank@pobox.com
+// Project started 11/12/2017
+//
+// 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 <http://www.gnu.org/licenses/>.
+//
+
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <fcntl.h>
+#include <sys/ioctl.h>
+#include <linux/types.h>
+#include "armbianio.h"
+#include <linux/spi/spidev.h>
+#include <linux/i2c-dev.h>
+#include <pthread.h>
+#include <poll.h>
+
+static struct spi_ioc_transfer xfer;
+// Maximum header pins for all supported boards
+#define MAX_PINS IR_PIN+1
+static int iPinHandles[MAX_PINS]; // keep file handles open for GPIO access
+static AIOCALLBACK cbList[MAX_PINS];
+static AIOIRCALLBACK cbIRList[MAX_PINS];
+//
+// The following are lists which translate pin numbers into the GPIO numbers
+// used by the different boards. The first entry (0) is for the on-board
+// button (if present). A -1 indicates that the pin is not available to use
+// as a GPIO (e.g. +5V, GND, etc). The numbering normally starts with the 3.3v
+// pin as 1 and the 5V pin as 2. The TTY header comes after the last GPIO pin
+// and is numbered from the pin closest to the edge of the board. On the
+// Orange Pi Zero, this means that GND is pin 27, RX is 28 and TX is 29. The
+// reason these are included is because they are multiplexed inside the SoC
+// and can be used for GPIOs as well.
+//
+
+// Le potato
+static int ipotatoPins[] = {-1,-1,-1,5,-1,4,-1,108,101,-1,
+			102,-1,6,9,-1,110,103,-1,104,97,
+			-1,98,89,100,99,-1,90,85,86,106,
+			-1,107,105,95,-1,96,91,94,92,-1,
+			93};
+
+// Banana Pi M2 Zero
+static int iBPIZPins[] = {355,-1,-1,12,-1,11,-1,6,13,-1,
+			14,1,110,0,-1,3,15,-1,68,64,
+			-1,65,2,66,67,-1,71,19,18,7,
+			-1,8,354,9,-1,10,356,17,21,-1,
+			20};
+
+// Raspberry Pi
+static int iRPIPins[] = {-1,-1,-1,2,-1,3,-1,4,14,-1,
+                        15,17,18,27,-1,22,23,-1,24,10,
+                        -1,9,25,11,8,-1,7,0,1,5,
+                        -1,6,12,13,-1,19,16,26,20,-1,
+                        21};
+static int iWiringPiPins[] = {-1,-1,-1,8,-1,9,-1,7,
+			15,-1,16,0,1,2,-1,3,
+			4,-1,5,12,-1,13,6,14,
+			10,-1,11,30,31,21,-1,22,
+			26,23,-1,24,27,25,28,-1,
+			29};
+
+
+// Orange Pi Zero Plus
+static int iOPIZPPins[] = {-1,-1,-1,12,-1,11,-1,6,198,-1,
+                        199,1,7,0,-1,3,19,-1,18,15,
+                        -1,16,2,14,13,-1,10,-1,5,4}; // last 3 pins are TTY header
+
+// Orange Pi Zero Plus 2
+static int iOPIZP2ins[] = {-1,-1,-1,12,-1,11,-1,6,0,-1,
+			1,352,107,353,-1,3,19,-1,18,-1,
+			-1,-1,2,14,13,-1,110,-1,5,4}; // last 3 pins are TTY header
+
+// Orange Pi One
+static int iOPI1Pins[] = {355,-1,-1,12,-1,11,-1,6,13,-1,
+			14,1,110,0,-1,3,68,-1,71,64,
+			-1,65,2,66,67,-1,21,19,18,7,
+			-1,8,200,9,-1,10,201,20,198,-1
+			,199,4,5,-1}; // last 3 pins are TTY header
+
+// Orange Pi Zero
+static int iOPIZPins[] = {-1,-1,-1,12,-1,11,-1,6,198,-1,
+			199,1,7,0,-1,3,19,-1,18,15,
+			-1,16,2,14,13,-1,10,-1,5,4}; // last 3 pins are TTY header
+
+// NanoPi Duo
+static int iNPDPins[] = {355,5,-1,4,-1,-1,-1,11,-1,12,
+			363,13,203,14,-1,16,-1,15,-1,199,
+			-1,198,-1,-1,-1,-1,-1,-1,-1,-1,
+			-1,-1,-1};
+
+// NanoPi 2
+static int iNP2Pins[] = {0,-1,-1,99,-1,98,-1,32+28,96+21,-1,
+			96+17,32+29,32+26,32+30,-1,32+31,64+14,-1,32+27,64+31,
+			-1,96+0,96+1,64+29,64+30,-1,64+13,103,102,64+8,
+			-1,64+9,64+28,64+10,-1,64+12,64+7,64+11,162,-1,
+			163};
+
+// NanoPi K2
+static int iNPK2Pins[] = {3,-1,-1,205,-1,206,-1,211,102,-1,
+			225,212,227,213,-1,214,226,-1,215,216,
+			-1,218,217,220,219,-1,221,207,208,222,
+			-1,127,223,155,-1,252,-1,-1,-1,-1,
+			-1};
+
+// NanoPi Neo & NanoPi Air & NanoPi Neo 2
+static int iNPNPins[] = {-1,-1,-1,12,-1,11,-1,203,198,-1,
+			199,0,6,2,-1,3,200,-1,201,64,
+			-1,65,1,66,67,-1,-1,-1,-1,-1,
+			363,17,-1,-1,-1,-1,-1,-1,-1,4,
+			5};
+
+// NanoPi M4
+static int iNPM4Pins[] = {-1,-1,-1,-1,-1,-1,-1,32,145,-1,144,
+            33,50,35,-1,36,54,-1,55,-1,-1,
+            -1,56,-1,-1,-1,149,-1,-1,-1,
+            -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1};
+
+// Tinkerboard
+static int iTinkerPins[] = {-1,-1,-1,252,-1,253,-1,17,161,-1,
+			160,164,184,166,-1,167,162,-1,163,257,
+			-1,256,171,254,255,-1,251,233,234,165,
+			-1,168,239,238,-1,185,223,224,187,-1,
+			188};
+
+static int *iPinLists[] = {ipotatoPins, iBPIZPins, iRPIPins, iOPIZPPins, iOPIZP2ins, iOPIZPins, iOPI1Pins, iOPI1Pins, iNPDPins, iNP2Pins, iNPK2Pins, iNPNPins, iNPNPins, iNPNPins, iNPM4Pins, iNPM4Pins, iTinkerPins};
+static const char *szBoardNames[] = {"Le potato\n","Banana Pi M2 Zero\n","Raspberry Pi","Orange Pi Zero Plus\n","Orange Pi Zero Plus 2\n","Orange Pi Zero\n","Orange Pi Lite\n","Orange Pi One\n","NanoPi Duo\n", "NanoPi 2\n", "Nanopi K2\n", "NanoPi Neo\n", "NanoPi Air\n", "NanoPi Neo 2\n", "NanoPi M4\n", "NanoPi M4V2\n", "Tinkerboard\n",NULL};
+static int iBoardType;
+static int iPinCount[] = {40,40,40,29,29,29,43,43,32,40,40,40,40,40,40, 41,41}; // number of pins in the header
+// GPIO number of on-board IR receiver
+static int iIR_GPIO[] = {7, 0, 0, 363, 363, 363, 363, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+
+//
+// Close any open handles to GPIO pins and
+// 'unexport' them
+//
+void AIOShutdown(void)
+{
+int i;
+	for (i=0; i<MAX_PINS; i++) // try to close/release all open GPIO pins
+	{
+		AIORemoveGPIO(i);
+	}
+} /* AIOShutdown() */
+
+//
+// Initialize the ArmbianIO library
+// Convenience function calls AIOInitBoard with NULL
+//
+int AIOInit(void)
+{
+	return AIOInitBoard(NULL);
+}
+
+//
+// Initialize the ArmbianIO library
+// Determines the board type (name) and initializes the 'key' if present
+//
+int AIOInitBoard(const char *pBoardName)
+{
+FILE *ihandle;
+char szTemp[256];
+int i;
+
+// Determine what board we're running on to know which GPIO
+// pin number table to use
+
+	szTemp[0] = 0;
+	if (pBoardName)
+	{
+		strcpy(szTemp, pBoardName);
+	}
+	else
+	{
+		ihandle = fopen("/run/machine.id", "rb");
+		if (ihandle != NULL)
+		{
+			i = fread(szTemp, 1, 255, ihandle);
+			fclose(ihandle);
+			szTemp[i] = 0; // make sure it's zero terminated
+		}
+	}
+	// see if the board name matches known names
+	i = 0;
+	iBoardType = 2;
+	while (szBoardNames[i] != NULL)
+	{
+		if (strcmp(szBoardNames[i], szTemp) == 0) // found it!
+		{
+			iBoardType = i;
+			break;
+		}
+		i++;
+	}
+	if (iBoardType == -1) // not found
+	{
+		fprintf(stderr, "Unrecognized board type, aborting...\n");
+		return 0;
+	}
+	// Initialize all GPIO file system handles to -1 to start
+	memset(iPinHandles, -1, sizeof(iPinHandles));
+	// Try to activate the GPIO input for the key/button (if there is one)
+	AIOAddGPIO(0, GPIO_IN);
+	return 1; // success
+} /* AIOInit() */
+
+//
+// Boolean indicating if the current PCB has an on-board IR receiver module
+//
+int AIOHasIR(void)
+{
+	if (iBoardType != -1)
+	{
+		return (iIR_GPIO[iBoardType] != 0);
+	}
+	return 0;
+} /* AIOHasIR() */
+
+//
+// Boolean indicating if the current PCB has a button/key on it
+//
+int AIOHasButton(void)
+{
+int *pPins;
+
+	if (iBoardType != -1)
+	{
+		pPins = iPinLists[iBoardType];
+		return (pPins[0] != -1); // if the GPIO number at index 0 is valid
+	}
+	return 0;
+} /* AIOHasButton() */
+
+//
+// Read the boolean value of the key/button on the board
+// A 0 indicates pressed (pulled to ground) and 1 indicates not pressed
+//
+int AIOReadButton(void)
+{
+
+	if (iBoardType != -1)
+	{
+		return AIOReadGPIO(0);
+	}
+	return 0;
+} /* AIOReadButton() */
+
+//
+// Returns the number of pins for the current board
+// This can include the 3 pins for the TTY header since the RX/TX lines can also
+// be used as ordinary GPIO inputs/outputs
+//
+int AIOPinCount(void)
+{
+	if (iBoardType != -1)
+	{
+		return iPinCount[iBoardType];
+	}
+	return 0;
+} /* AIOPinCount() */
+
+//
+// Simultaneous read/write to SPI bus
+//
+int AIOReadWriteSPI(int iHandle, unsigned char *pTxBuf, unsigned char *pRxBuf, int iLen)
+{
+int rc;
+	xfer.rx_buf = (unsigned long)pRxBuf;
+	xfer.tx_buf = (unsigned long)pTxBuf;
+	xfer.len = iLen;
+	rc = ioctl(iHandle, SPI_IOC_MESSAGE(1), &xfer);
+    	return rc;
+} /* AIOReadWriteSPI() */
+
+//
+// Read from SPI bus
+//
+int AIOReadSPI(int iHandle, unsigned char *buf, int iLen)
+{
+int rc;
+	xfer.rx_buf = (unsigned long)buf;
+	xfer.tx_buf = 0;
+	xfer.len = iLen;
+	rc = ioctl(iHandle, SPI_IOC_MESSAGE(1), &xfer);
+    	return rc;
+} /* AIOReadSPI() */
+
+//
+// Write to SPI bus
+//
+int AIOWriteSPI(int iHandle, unsigned char *pBuf, int iLen)
+{
+int rc;
+	xfer.rx_buf = 0;
+	xfer.tx_buf = (unsigned long)pBuf;
+	xfer.len = iLen;
+	rc = ioctl(iHandle, SPI_IOC_MESSAGE(1), &xfer);
+    	return rc;
+} /* AIOWriteSPI() */
+
+int AIOOpenI2C(int iChannel, int iAddress)
+{
+char filename[32];
+int file_i2c;
+
+	sprintf(filename, "/dev/i2c-%d", iChannel);
+	if ((file_i2c = open(filename, O_RDWR)) < 0)
+	{
+		fprintf(stderr, "Failed to open the i2c bus\n");
+		return -1;
+	}
+
+	if (ioctl(file_i2c, I2C_SLAVE, iAddress) < 0)
+	{
+		fprintf(stderr, "Failed to acquire bus access or talk to slave\n");
+		return -1;
+	}
+	return file_i2c;
+
+} /* AIOOpenI2C() */
+
+void AIOCloseI2C(int iHandle)
+{
+	close(iHandle);
+} /* AIOCloseI2C() */
+
+int AIOReadI2C(int iHandle, unsigned char ucRegister, unsigned char *buf, int iCount)
+{
+int rc;
+
+	// Reading from an I2C device involves first writing the 8-bit register
+	// followed by reading the data
+	rc = write(iHandle, &ucRegister, 1); // write the register value
+	if (rc == 1)
+	{
+		rc = read(iHandle, buf, iCount);
+	}
+	return rc;
+} /* AIOReadI2C() */
+
+int AIOWriteI2C(int iHandle, unsigned char ucRegister, unsigned char *buf, int iCount)
+{
+int rc;
+unsigned char ucTemp[2048];
+
+	// Writing to an I2C device involves first writing the 8-bit register
+	// followed by writing the data
+	ucTemp[0] = ucRegister; // some devices need it written atomically
+	memcpy(&ucTemp[1], buf, iCount);
+	rc = write(iHandle, ucTemp, iCount+1);
+	return rc;
+} /* AIOWriteI2C() */
+
+//
+// Read from a GPIO pin
+//
+int AIOReadGPIO(int iPin)
+{
+int iGPIO = 0;
+char szTemp[64];
+int rc;
+int *pPins;
+
+	if (iBoardType == -1) // library not initialized
+		return -1;
+	if (iPin < 0 || (iPin != IR_PIN && iPin > iPinCount[iBoardType])) // invalid pin number for this board
+		return -1;
+	if (iPin == IR_PIN && iIR_GPIO[iBoardType] == 0) // no IR receiver
+		return -1;
+	if (iPinHandles[iPin] == -1)
+	{
+		pPins = iPinLists[iBoardType];
+		if (iPin == IR_PIN)
+			iGPIO = iIR_GPIO[iBoardType];
+		else
+			iGPIO = pPins[iPin];
+		sprintf(szTemp, "/sys/class/gpio/gpio%d/value", iGPIO);
+		iPinHandles[iPin] = open(szTemp, O_RDONLY);
+	}
+	lseek(iPinHandles[iPin], 0, SEEK_SET); // reset file pointer to start
+	rc = read(iPinHandles[iPin], szTemp, 1);
+	if (rc <= 0) // problem
+	{
+		fprintf(stderr, "Error reading from GPIO %d\n", iGPIO);
+		return -1;
+	}
+	return (szTemp[0] == '1');
+} /* AIOReadGPIO() */
+
+//
+// Write a 0 or 1 to a GPIO output line
+//
+int AIOWriteGPIO(int iPin, int iValue)
+{
+int rc, iGPIO;
+char szTemp[64];
+int *pPins;
+
+	if (iBoardType == -1) // not initialized
+		return 0;
+	if (iPin < 1 || iPin > iPinCount[iBoardType])
+		return 0;
+	if (iPinHandles[iPin] == -1) // not open yet
+	{
+		pPins = iPinLists[iBoardType];
+		iGPIO = pPins[iPin]; // convert to GPIO number
+		sprintf(szTemp, "/sys/class/gpio/gpio%d/value", iGPIO);
+		iPinHandles[iPin] = open(szTemp, O_WRONLY);
+	}
+	if (iValue) rc = write(iPinHandles[iPin], "1", 1);
+	else rc = write(iPinHandles[iPin], "0", 1);
+	if (rc < 0) // error
+	{ // do something
+	}
+	return 1;
+} /* AIOWriteGPIO() */
+
+//
+// Set edge value for an open pin
+//
+int AIOWriteGPIOEdge(int iPin, int iEdge)
+{
+char szName[64];
+int file_gpio, rc, iGPIO;
+int *pPins;
+char *szEdges[] = {"falling\n","rising\n","both\n","none\n"};
+
+	if (iEdge < EDGE_FALLING || iEdge > EDGE_NONE)
+		return 0;
+	if (iBoardType == -1) // not initialized
+		return 0;
+	if (iPin < 0 || (iPin != IR_PIN && iPin > iPinCount[iBoardType]))
+		return 0;
+	pPins = iPinLists[iBoardType];
+	// Set the mapped pin
+	if (iPin == IR_PIN)
+		iGPIO = iIR_GPIO[iBoardType];
+	else
+		iGPIO = pPins[iPin];
+	sprintf(szName, "/sys/class/gpio/gpio%d/edge", iGPIO);
+	file_gpio = open(szName, O_WRONLY);
+	// Write edge type
+	rc = write(file_gpio, szEdges[iEdge], strlen(szEdges[iEdge]));
+	close(file_gpio);
+	if (rc < 0) // error
+	{ // do something
+	}
+	return 1;
+} /* AIOWriteGPIOEdge() */
+
+//
+// GPIO Monitoring thread (one for each pin)
+//
+void *GPIOThread(void *param)
+{
+int iPin = (int)param; // pin number is passed in
+struct pollfd fdset[1];
+char szName[32], szTemp[64];
+int gpio_fd;
+int *pPins, rc, iGPIO;
+int timeout = 3000; // 3 seconds
+
+	pPins = iPinLists[iBoardType];
+
+	if (iPin == IR_PIN)
+		iGPIO = iIR_GPIO[iBoardType];
+	else
+		iGPIO = pPins[iPin];
+	sprintf(szName, "/sys/class/gpio/gpio%d/value", iGPIO);
+	gpio_fd = open(szName, O_RDONLY);
+	if (gpio_fd < 0) // something went wrong
+		return NULL;
+	lseek(gpio_fd, 0, SEEK_SET);
+	rc = read(gpio_fd, szTemp, 64); // initial read to prevent false interrupt
+
+	while (1)
+	{
+		// If the callback is NULL then exit thread
+		if (cbList[iPin] == NULL)
+			return NULL;
+		memset(fdset, 0, sizeof(fdset));
+		fdset[0].fd = gpio_fd;
+		fdset[0].events = POLLPRI;
+		rc = poll(&fdset[0], 1, timeout);
+		if (rc < 0) return NULL;
+		// clear the interrupt by reading the data
+		lseek(gpio_fd, 0, SEEK_SET);
+		rc = read(gpio_fd, szTemp, 64);
+		// see if it was a valid interrupt event
+		if (fdset[0].revents & POLLPRI)
+		{
+			if (cbList[iPin])
+				(*cbList[iPin])(iPin);
+		}
+	}
+	return NULL;
+} /* GPIOThread() */
+
+//
+// Set edge to call the given function when the state
+// changes. AIOAddGPIO must be called first with direction
+// set to GPIO_IN
+//
+int AIOAddGPIOCallback(int iPin, AIOCALLBACK callback)
+{
+int *pPins;
+pthread_t tinfo;
+
+	if (iBoardType == -1) // not initialize
+		return 0;
+	pPins = iPinLists[iBoardType];
+	if (iPin != IR_PIN && pPins[iPin] == -1) // invalid pin
+		return 0;
+	cbList[iPin] = callback; // save the callback pointer
+	// Start a thread to manage the interrupt/callback
+	pthread_create(&tinfo, NULL, GPIOThread, (void *)iPin);
+	return 1;
+} /* AIOAddGPIOCallback() */
+
+//
+// Set pointer in callback list to NULL to cause
+// thread to exit
+//
+int AIORemoveGPIOCallback(int iPin)
+{
+
+	if (iBoardType == -1) // not initialize
+		return 0;
+	if (cbList[iPin] == NULL) // invalid pin
+		return 0;
+	cbList[iPin] = NULL; // This will force thread to exit
+	return 1;
+} /* AIORemoveGPIOCallback() */
+
+
+long getTimeInMicroseconds()
+{
+	struct timespec time_p;
+	clock_gettime(CLOCK_MONOTONIC, &time_p);
+	return (time_p.tv_sec % 10) * 1000000 + time_p.tv_nsec / 1000;
+}
+
+//
+// GPIO Monitoring thread for IR (one for each pin)
+// Code length must be less than 50.
+//
+void *GPIOIRThread(void *param)
+{
+int iPin = (int)param; // pin number is passed in
+int endOfCodeTimeOut = 3; // after 3 ms we think code has ended.
+struct pollfd fdset[1];
+char szName[32], szTemp[64];
+int gpio_fd;
+int *pPins, rc, iGPIO;
+int timeout = 3000; // 3 seconds
+long start = getTimeInMicroseconds();
+int currentCode[52];
+int i, codePointer = 0;
+
+	pPins = iPinLists[iBoardType];
+
+	if (iPin == IR_PIN)
+		iGPIO = iIR_GPIO[iBoardType];
+	else
+		iGPIO = pPins[iPin];
+	sprintf(szName, "/sys/class/gpio/gpio%d/value", iGPIO);
+	gpio_fd = open(szName, O_RDONLY);
+	if (gpio_fd < 0) // something went wrong
+		return NULL;
+	lseek(gpio_fd, 0, SEEK_SET);
+	rc = read(gpio_fd, szTemp, 64); // initial read to prevent false interrupt
+
+	while (1)
+	{
+		// If the callback is NULL then exit thread
+		if (cbIRList[iPin] == NULL)
+			return NULL;
+		memset(fdset, 0, sizeof(fdset));
+		fdset[0].fd = gpio_fd;
+		fdset[0].events = POLLPRI;
+		rc = poll(&fdset[0], 1, timeout);
+		if (rc < 0) return NULL;
+		// clear the interrupt by reading the data
+		lseek(gpio_fd, 0, SEEK_SET);
+		rc = read(gpio_fd, szTemp, 64);
+		// see if it was a valid interrupt event
+		if (fdset[0].revents & POLLPRI)
+		{
+			long now = getTimeInMicroseconds();
+			long between = now - start;
+//			printf("between: %11d\n", between);
+			currentCode[codePointer] = (int) between;
+			codePointer++;
+			timeout = endOfCodeTimeOut;
+			start = now;
+//			printf("cp: %d", codePointer);
+			if(codePointer > 50) { // max code length reached, send what we have.
+			    if (cbIRList[iPin])
+                    (*cbIRList[iPin])(currentCode);
+				codePointer = 0;
+				for(i = 0; i < 50; i++) { currentCode[i] = 0; }
+			}
+		} else if(timeout == endOfCodeTimeOut) { // are we receiving a code currently?
+            //printf("Between: %d\n", getTimeInMicroseconds() - start);
+            if (cbIRList[iPin])
+                (*cbIRList[iPin])(currentCode);
+			timeout = 3000; // wait for next code again, with the default timeout
+			codePointer = 0;
+			for(i = 0; i < 50; i++) { currentCode[i] = 0; }
+		} else {
+		    printf("Code timeout\n");
+		}
+	}
+	return NULL;
+} /* GPIOIRThread() */
+
+
+int AIOAddGPIOIRCallback(int iPin, AIOIRCALLBACK callback)
+{
+int *pPins;
+pthread_t tinfo;
+
+	if (iBoardType == -1) // not initialize
+		return 0;
+	pPins = iPinLists[iBoardType];
+	if (iPin != IR_PIN && pPins[iPin] == -1) // invalid pin
+		return 0;
+	cbIRList[iPin] = callback; // save the callback pointer
+	// Start a thread to manage the interrupt/callback
+	pthread_create(&tinfo, NULL, GPIOIRThread, (void *)iPin);
+	return 1;
+} /* AIOAddGPIOCallback() */
+
+
+int AIORemoveGPIOIRCallback(int iPin)
+{
+
+	if (iBoardType == -1) // not initialize
+		return 0;
+	if (cbIRList[iPin] == NULL) // invalid pin
+		return 0;
+	cbIRList[iPin] = NULL; // This will force thread to exit
+	return 1;
+} /* AIORemoveGPIOIRCallback() */
+
+//
+// Initialize a GPIO line for input or output
+// This will export it to the sysfs driver and
+// it will appear in /sys/class/gpio
+//
+int AIOAddGPIO(int iPin, int iDirection)
+{
+char szName[64];
+int file_gpio, rc, iGPIO;
+int *pPins;
+
+	if (iBoardType == -1) // not initialize
+		return 0;
+	pPins = iPinLists[iBoardType];
+	if (iPin != IR_PIN && pPins[iPin] == -1) // invalid pin
+		return 0;
+	if (iPin == IR_PIN && iIR_GPIO[iBoardType] == 0)
+		return 0; // invalid IR pin
+	file_gpio = open("/sys/class/gpio/export", O_WRONLY);
+	if (iPin == IR_PIN)
+		iGPIO = iIR_GPIO[iBoardType];
+	else
+		iGPIO = pPins[iPin];
+	sprintf(szName, "%d", iGPIO);
+	rc = write(file_gpio, szName, strlen(szName));
+	close(file_gpio);
+	sprintf(szName, "/sys/class/gpio/gpio%d/direction", iGPIO);
+	file_gpio = open(szName, O_WRONLY);
+	if (iDirection == GPIO_OUT)
+		rc = write(file_gpio, "out\n", 4);
+	else
+		rc = write(file_gpio, "in\n", 3);
+	close(file_gpio);
+	if (iDirection == GPIO_IN_PULLUP) // RPI specific feature, use GPIO library
+	{
+		sprintf(szName, "gpio mode %d up", iWiringPiPins[iPin]);
+		system(szName);
+	}
+	if (rc < 0) // added to suppress compiler warnings
+	{ // do nothing
+	}
+	return 1;
+} /* AIOAddGPIO() */
+
+//
+// Remove access to a GPIO pin
+// This will 'unexport' it from the sysfs driver
+// and remove it from the /sys/class/gpio directory
+//
+void AIORemoveGPIO(int iPin)
+{
+int file_gpio, rc;
+char szTemp[64];
+int *pPins;
+
+	if (iBoardType == -1) // not initialized
+		return;
+	if (iPin < 1 || iPin > MAX_PINS) // invalid pin
+		return;
+	if (iPinHandles[iPin] != -1)
+	{
+		close(iPinHandles[iPin]);
+		file_gpio = open("/sys/class/gpio/unexport", O_WRONLY);
+		pPins = iPinLists[iBoardType];
+		sprintf(szTemp, "%d", pPins[iPin]);
+		rc = write(file_gpio, szTemp, strlen(szTemp));
+		close(file_gpio);
+		if (rc < 0) // suppress compiler warning
+		{ // do nothing
+		}
+		iPinHandles[iPin] = -1;
+	}
+} /* AIORemoveGPIO() */
+
+//
+// Open a handle to the SPI bus
+//
+int AIOOpenSPI(int iChannel, int iSPIFreq)
+{
+int rc, iSPIMode = SPI_MODE_0; // | SPI_NO_CS;
+char szName[32];
+int file_spi;
+int i = iSPIFreq;
+
+	sprintf(szName,"/dev/spidev%d.0", iChannel);
+	file_spi = open(szName, O_RDWR);
+	rc = ioctl(file_spi, SPI_IOC_WR_MODE, &iSPIMode);
+	if (rc < 0) fprintf(stderr, "Error setting SPI mode\n");
+	rc = ioctl(file_spi, SPI_IOC_WR_MAX_SPEED_HZ, &i);
+	if (rc < 0) fprintf(stderr, "Error setting SPI speed\n");
+	memset(&xfer, 0, sizeof(xfer));
+	xfer.speed_hz = iSPIFreq;
+	xfer.cs_change = 0;
+	xfer.delay_usecs = 0;
+	xfer.bits_per_word = 8;
+
+	if (file_spi < 0)
+	{
+		fprintf(stderr, "Failed to open the SPI bus\n");
+		return -1;
+	}
+	return file_spi;
+} /* AIOOpenSPI() */
+
+void AIOCloseSPI(int iHandle)
+{
+	close(iHandle);
+} /* AIOCloseSPI() */
+
+//
+// Returns the name of the board (if recognized)
+//
+const char * AIOGetBoardName(void)
+{
+	if (iBoardType == -1)
+		return "Unknown";
+	else
+		return szBoardNames[iBoardType];
+} /* AIOGetBoardName() */

+ 185 - 0
armbianio.h

@@ -0,0 +1,185 @@
+#ifndef _ARMBIANIO_H
+#define _ARMBIANIO_H
+//
+// SPI_LCD using the SPI interface
+// Copyright (c) 2017 Larry Bank
+// email: bitbank@pobox.com
+// Project started 4/25/2017
+//
+// 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 <http://www.gnu.org/licenses/>.
+//
+#define GPIO_OUT 0
+#define GPIO_IN 1
+// Only available on RPI boards
+#define GPIO_IN_PULLUP 2
+#define EDGE_FALLING 0
+#define EDGE_RISING 1
+#define EDGE_BOTH 2
+#define EDGE_NONE 3
+// Virtual header pin number to access the on-board IR receiver module
+// with GPIO functions (since it's just a digital input connected to a GPIO)
+#define IR_PIN 50
+#ifdef __cplusplus
+extern "C" {
+#endif
+//
+// Initialize the library
+// 1 = success, 0 = failure
+//
+// Convenience function to behave like original API. Calls AIOInitBoard with
+// NULL
+//
+int AIOInit(void);
+
+//
+// Initialize the library
+// 1 = success, 0 = failure
+//
+// On Armbian 4.1x, pBoardName can be NULL and it will read the name internally
+// for non-Armbian (e.g. Raspberry Pi) or earlier builds of Armbian, pass the
+// board name to get the correct GPIO pin mappings
+//
+int AIOInitBoard(const char *pBoardName);
+
+
+// Free the resources
+void AIOShutdown(void);
+
+//
+// Returns the name of the board you're running on
+// or "Unknown" for an unsupported board
+//
+const char * AIOGetBoardName(void);
+
+//
+// Returns a file handle to the I2C device and address specified
+// -1 if it fails to open
+//
+int AIOOpenI2C(int iChannel, int iAddress);
+
+//
+// Returns a file handle to the SPI device specified
+// -1 if it fails to open
+//
+int AIOOpenSPI(int iChannel, int iSpeed);
+
+//
+// Close the file handle for the I2C bus
+//
+void AIOCloseI2C(int iHandle);
+
+//
+// Close the file handle for the SPI bus
+//
+void AIOCloseSPI(int iHandle);
+
+//
+// Read bytes from the I2C device
+// Pass the "starting" register number
+// Returns the number of bytes read or -1 for error
+//
+int AIOReadI2C(int iHandle, unsigned char ucRegister, unsigned char *buf, int iCount);
+
+//
+// Write data to the I2C device starting at the given register
+// returns the number of bytes written or -1 for error
+//
+int AIOWriteI2C(int iHandle, unsigned char ucRegister, unsigned char *buf, int iCount);
+
+//
+// Read data from the SPI device
+// returns the number of bytes read or -1 for error
+//
+int AIOReadSPI(int iHandle, unsigned char *buf, int iCount);
+
+//
+// Write data to the SPI device
+// returns the number of bytes written or -1 for error
+//
+int AIOWriteSPI(int iHandle, unsigned char *buf, int iCount);
+
+//
+// Perform a simultaneous read and write on the SPI device
+// returns the number of bytes transferred or -1 for error
+//
+int AIOReadWriteSPI(int iHandle, unsigned char *inbuf, unsigned char *outbuf, int iCount);
+
+//
+// Boolean indicating if the current PCB has a built-in IR receiver
+//
+int AIOHasIR(void);
+
+//
+// Boolean indicating if the current PCB has a button/key on it
+//
+int AIOHasButton(void);
+
+//
+// Read the button on the PCB (if present)
+//
+int AIOReadButton(void);
+
+//
+// Configure a GPIO pin for input or output
+// (GPIO_IN or GPIO_OUT)
+//
+int AIOAddGPIO(int iPin, int iDirection);
+
+typedef void (*AIOCALLBACK)(int iPin);
+
+//
+// Set edge to call the given function when the state
+// changes. AIOAddGPIO must be called first with direction
+// set to GPIO_IN
+//
+int AIOAddGPIOCallback(int iPin, AIOCALLBACK callback);
+
+//
+// Set pointer in callback list to NULL and set edge
+// to none
+//
+int AIORemoveGPIOCallback(int iPin);
+
+// The IR counterparts
+typedef void (*AIOIRCALLBACK)(int *codeArray);
+int AIOAddGPIOIRCallback(int iPin, AIOIRCALLBACK callback);
+int AIORemoveGPIOIRCallback(int iPin);
+
+//
+// Release a GPIO pin
+//
+void AIORemoveGPIO(int iPin);
+
+//
+// Read the state of a GPIO input pin
+// returns 0 or 1
+//
+int AIOReadGPIO(int iPin);
+
+//
+// Sets the state of a GPIO output pin
+// Valid states are 1 (on) or 0 (off)
+//
+int AIOWriteGPIO(int iPin, int iValue);
+
+//
+// Set edge value for an open pin
+//
+int AIOWriteGPIOEdge(int iPin, int iEdge);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // _ARMBIANIO_H

+ 68 - 0
helper.h

@@ -0,0 +1,68 @@
+#ifndef HELPER_H
+#define HELPER_H
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <unistd.h>
+#include <string.h>
+#include <time.h>
+#include <math.h>
+
+const char EASE_IN[] = "@\x15\r\t\x07\x06\x05\x04\x04\x03\x03\x03\x03\x02\x02\x02\x02\x02\x02\x02\x02";
+const char EASE_OUT[] = "\x02\x02\x02\x02\x02\x02\x02\x02\x03\x03\x03\x03\x04\x04\x05\x06\x07\t\r\x15@";
+
+void umlauts(char *cTemp, int cLen) {
+	for(int i=0; i<cLen-1; i++) {
+		if(cTemp[i] == 0xC2) {
+			if(cTemp[i+1] == 0xB0) {//°
+				cTemp[i] = '\'';
+				if(i < cLen-2) {
+					memcpy(&cTemp[i+1], &cTemp[i+2], cLen-2-i);
+					cTemp[cLen-1] = '\0';
+				}
+				else
+					cTemp[i+1] = '\0';
+			}
+		} else if(cTemp[i] == 0xC3) {
+			switch (cTemp[i+1]) {
+			case 0xA4:
+				memcpy(&cTemp[i], "ae", 2);
+				break;
+			case 0xB6:
+				memcpy(&cTemp[i], "oe", 2);
+				break;
+			case 0xBC:
+				memcpy(&cTemp[i], "ue", 2);
+				break;
+			case 0x84:
+				memcpy(&cTemp[i], "Ae", 2);
+				break;
+			case 0x96:
+				memcpy(&cTemp[i], "Oe", 2);
+				break;
+			case 0x9C:
+				memcpy(&cTemp[i], "Ue", 2);
+				break;
+			case 0x9F:
+				memcpy(&cTemp[i], "ss", 2);
+				break;
+			default:
+				break;
+			}
+		}
+	}
+}
+
+void debugPrint(uint8_t *bImg, int iPitch) {
+	for(int row=0; row<8; row++) {
+		for(int col=0; col<iPitch*8; col++) {
+			char c = ' ';
+			if(bImg[col/8+row*iPitch] & (1 << (7-col%8))) c = '#';
+			putchar(c);
+		}
+		putchar('\n');
+	}
+}
+
+#endif

+ 424 - 0
main.c

@@ -0,0 +1,424 @@
+//
+// MAX7219 demo program
+//
+// Copyright (c) 2018 BitBank Software, Inc.
+// Written by Larry Bank
+// email: bitbank@pobox.com
+// Project started 3/10/2018
+//
+// 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 <http://www.gnu.org/licenses/>.
+//
+//
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+#include <time.h>
+#include <math.h>
+
+#include <MQTTClient.h>
+#include<json-c/json.h>
+
+#include "max7219.h"
+#include "helper.h"
+#include "types.h"
+
+#define ADDRESS     "tcp://localhost:1883"
+#define CLIENTID    "max7219_c"
+#define QOS         1
+#define TIMEOUT     10000L
+
+volatile MQTTClient_deliveryToken deliveredtoken;
+struct scrolltext_t stWeather, stDisplay, stTime, stDate;
+
+int drawString(struct scrolltext_t *st, char *cTemp) {
+	if(st->width+6 >= SCROLL_BUF_SIZE) {
+		printf("ERROR: no space for scrolltext\n");
+		return 0;
+	}
+	char buf[64];
+	snprintf(buf, sizeof(buf), "%s", cTemp);
+	int width = strlen(buf)*6;
+	if(width + st->width > SCROLL_BUF_SIZE) {
+		width = (SCROLL_BUF_SIZE - st->width)/6*6;
+		buf[width/6] = '\0';
+		printf("WARNING: scrolltext cropped\n");
+	}
+
+	maxScrollBitmap(st->bImg, SCROLL_BUF_SIZE/8, (st->width % 8));
+	maxDrawString(buf, (st->bImg) + st->width/8, SCROLL_BUF_SIZE/8, 1); // draw narrow digits
+	maxScrollBitmap(st->bImg, SCROLL_BUF_SIZE/8, -(st->width % 8));
+	printf("new scroll text, offset: %d:%d, width: %d, text: %s\n", st->width/8, st->width%8, width, buf);
+	st->width = st->width + width;
+	return width;
+}
+
+int drawGraph(struct scrolltext_t *st, struct weather_t data[], size_t field_offset, int count, float min, float max) {
+	int width = st->width;
+	for(int i=0; i<count; i++) {
+		if(data[i].valid) {
+			if(i == 31 && st->width + sizeof(EASE_IN) < SCROLL_BUF_SIZE) {
+				memcpy(st->frameCount + st->width-sizeof(EASE_OUT)+2, EASE_OUT, sizeof(EASE_OUT)-1);
+				memcpy(st->frameCount + st->width+1, EASE_IN, sizeof(EASE_IN)-1);
+				st->frameCount[st->width+1] = 0xFF;
+			}
+			int col = st->width++;
+			float field = *((float*)(&data[i].valid+field_offset/sizeof(float)));
+			int val = fmax(fmin((field-min) / (max-min) * 8, 7), 0);
+			st->bImg[col/8 + (7-val)*SCROLL_BUF_SIZE/8] |= 0x80 >> (col%8);
+		}
+	}
+	return st->width - width;
+}
+
+void stReset(struct scrolltext_t *st) {
+	memset(st->bImg, 0, SCROLL_BUF_SIZE);
+	st->width = 0;
+	st->frame = 0;
+	memset(st->frameCount, 2, sizeof(st->frameCount));
+	memcpy(st->frameCount, EASE_IN, sizeof(EASE_IN)-1);
+}
+
+void delivered(void *context, MQTTClient_deliveryToken dt)
+{
+    printf("Message with token value %d delivery confirmed\n", dt);
+    deliveredtoken = dt;
+}
+int msgarrvd(void *context, char *topicName, int topicLen, MQTTClient_message *message)
+{
+    int i;
+    char* payloadptr;
+    printf("Message arrived\n");
+    printf("     topic: %s\n", topicName);
+    printf("   message: ");
+		umlauts(message->payload, message->payloadlen);
+    payloadptr = message->payload;
+    for(i=0; i<message->payloadlen; i++)
+    {
+        putchar(*payloadptr);
+				if(*payloadptr > 0x7F)
+					printf("[%X]", *payloadptr);
+				payloadptr++;
+				if (i >= 300) break;
+    }
+    putchar('\n');
+
+		if(strcmp(topicName, "Room/display") == 0) {
+			char displayMsg[128];
+			snprintf(displayMsg, sizeof(displayMsg), "%s", (char*)message->payload);
+			stReset(&stDisplay);
+			drawString(&stDisplay, displayMsg);
+		} else if(strcmp(topicName, "Timer/weather") == 0) {
+			struct json_object *parsed_json, *detail, *tempc, *humidity, *clouds, *windspeed;
+			parsed_json = json_tokener_parse(message->payload);
+			json_object_object_get_ex(parsed_json, "detail", &detail);
+			json_object_object_get_ex(parsed_json, "tempc", &tempc);
+			json_object_object_get_ex(parsed_json, "humidity", &humidity);
+			json_object_object_get_ex(parsed_json, "clouds", &clouds);
+			json_object_object_get_ex(parsed_json, "windspeed", &windspeed);
+			char weatherMsg[128];
+			snprintf(
+				weatherMsg, sizeof(weatherMsg),
+				"%s %.1f'C LF %d%% WLK %d%% %.1f m/s",
+				json_object_get_string(detail),
+				json_object_get_double(tempc),
+				json_object_get_int(humidity),
+				json_object_get_int(clouds),
+				json_object_get_double(windspeed)
+			);
+
+		} else if(strcmp(topicName, "Timer/forecast") == 0) {
+			time_t rawtime;
+			time(&rawtime);
+			struct json_object *parsed_json, *forecast, *tmp, *fmain, *weather, *clouds, *rain, *snow, *wind;
+			parsed_json = json_tokener_parse(message->payload);
+			int count = json_object_array_length(parsed_json);
+			if(count > 40)
+				count = 40;
+
+			struct weather_t data[40], maxData;
+			memset(data, 0, sizeof(data));
+			memset(&maxData, 0, sizeof(maxData));
+			int dataPoints = 0;
+			float temp_min = 100, rain_sum = 0, snow_sum = 0;
+
+			for(int i=0; i<count; i++) {
+				forecast = json_object_array_get_idx(parsed_json, i);
+				json_object_object_get_ex(forecast, "dt", &tmp);
+				time_t timestamp = json_object_get_uint64(tmp);
+				if(timestamp > rawtime) {
+					data[i].valid = 1;
+					dataPoints++;
+					data[i].dt = timestamp;
+					maxData.dt = timestamp;
+				} else {
+					continue;
+				}
+
+				float val;
+				json_object_object_get_ex(forecast, "main", &fmain);
+				if(json_object_object_get_ex(fmain, "temp", &tmp)) {
+					val = json_object_get_double(tmp);
+					data[i].temp = val;
+					if(val > maxData.temp) maxData.temp = val;
+					if(val < temp_min) temp_min = val;
+				}
+				json_object_object_get_ex(forecast, "weather", &weather);
+				if(json_object_object_get_ex(json_object_array_get_idx(weather, 0), "description", &tmp)) {
+					//const char *desc = json_object_get_string(tmp);
+				}
+				json_object_object_get_ex(forecast, "clouds", &clouds);
+				if(json_object_object_get_ex(clouds, "all", &tmp)) {
+					val = json_object_get_double(tmp);
+					data[i].clouds = val;
+					if(val > maxData.clouds) maxData.clouds = val;
+				}
+				json_object_object_get_ex(forecast, "wind", &wind);
+				if(json_object_object_get_ex(wind, "speed", &tmp)) {
+					val = json_object_get_double(tmp);
+					data[i].wind = val;
+					if(val > maxData.wind) maxData.wind = val;
+				}
+				json_object_object_get_ex(forecast, "rain", &rain);
+				if(json_object_object_get_ex(rain, "3h", &tmp)) {
+					val = json_object_get_double(tmp);
+					data[i].rain = val;
+				  if(val > maxData.rain) maxData.rain = val;
+					rain_sum += val;
+				}
+				json_object_object_get_ex(forecast, "snow", &snow);
+				if(json_object_object_get_ex(snow, "3h", &tmp)) {
+					val = json_object_get_double(tmp);
+					data[i].snow = val;
+					if(val > maxData.snow) maxData.snow = val;
+					snow_sum += val;
+				}
+			}
+			printf(
+				"dataPoints: %d, maxTemp: %.1f°C, maxClouds: %.1f%%, maxWind: %.1fm/s, rain: %.1fmm, snow: %.1fmm\n",
+				dataPoints,
+				maxData.temp,
+				maxData.clouds,
+				maxData.wind,
+				rain_sum,
+				snow_sum
+			);
+			char tmpStr[32];
+			struct tm *maxTm = localtime(&maxData.dt);
+			stReset(&stWeather);
+			sprintf(tmpStr, "Wetter bis %d.%d.: ", maxTm->tm_mday, maxTm->tm_mon+1);
+			drawString(&stWeather, tmpStr);
+			sprintf(tmpStr, "%.0f'C - %.0f'C ", temp_min, maxData.temp);
+			drawString(&stWeather, tmpStr);
+			
+			drawGraph(&stWeather, data, offsetof(struct weather_t, temp), count, temp_min, maxData.temp);
+			
+
+			if(maxData.wind > 3.0) {
+				sprintf(tmpStr, " Wind: %.1fm/s ", maxData.wind);
+				drawString(&stWeather, tmpStr);
+				drawGraph(&stWeather, data, offsetof(struct weather_t, wind), count, 0, maxData.wind);
+			}
+		  if(maxData.clouds > 10.0) {
+				sprintf(tmpStr, " Wlk: %.1f%% ", maxData.clouds);
+				drawString(&stWeather, tmpStr);
+				drawGraph(&stWeather, data, offsetof(struct weather_t, clouds), count, 0, maxData.clouds);
+			}
+			if(rain_sum > 1.0) {
+				sprintf(tmpStr, " Regen: %.1fmm ", rain_sum);
+				drawString(&stWeather, tmpStr);
+				drawGraph(&stWeather, data, offsetof(struct weather_t, rain), count, 0, maxData.rain);
+			}
+			if(snow_sum > 1.0) {
+				sprintf(tmpStr, " Schnee: %.1fmm ", snow_sum);
+				drawString(&stWeather, tmpStr);
+				drawGraph(&stWeather, data, offsetof(struct weather_t, snow), count, 0, maxData.snow);
+			}
+
+			json_object_put(parsed_json);
+		}
+
+    MQTTClient_freeMessage(&message);
+    MQTTClient_free(topicName);
+    return 1;
+}
+void connlost(void *context, char *cause)
+{
+    printf("\nConnection lost\n");
+    printf("     cause: %s\n", cause);
+}
+
+void drawTime(uint8_t *bImg, time_t *t) {
+	struct tm *now_tm = localtime(t);
+	char cTemp[6];
+	snprintf(
+		cTemp, sizeof(cTemp),
+		"%2d%c%02d",
+		now_tm->tm_hour,
+		now_tm->tm_sec%2 ? ':' : ' ',
+		now_tm->tm_min
+	);
+	memset(bImg, 0, 4*8);
+  maxDrawString(cTemp, bImg, 4, 1);
+}
+void drawDate(uint8_t *bImg, time_t *t) {
+	struct tm *now_tm = localtime(t);
+	char cTemp[10];
+	snprintf(
+		cTemp, sizeof(cTemp),
+		"%hhu.%hhu.",
+		now_tm->tm_mday,
+		now_tm->tm_mon+1
+	);
+	memset(bImg, 0, 4*8);
+	cTemp[6] = '\0';
+  maxDrawString(cTemp, bImg, 4, 1);
+}
+int drawScroll(uint8_t *bImg, struct scrolltext_t *st) {
+	for(int i=0; i<8; i++) {
+		if(st->offset < st->width && (st->bImg[i*SCROLL_BUF_SIZE/8 + st->offset/8] & (0x80 >> st->offset%8))) {
+			bImg[i*4+3] |= 0x01;
+		} else {
+			bImg[i*4+3] &= 0xFE;
+		}
+	}
+	return st->offset != 0;
+}
+int moveScroll(uint8_t *bImgCurrent, uint8_t *bImgTarget, struct scrolltext_t *st) {
+	int busy = 0;
+	int frames = (st->offset == 0) ? 0 : (st->frameCount)[st->offset-1];
+	if(st->width > 32 && ++(st->frame) >= frames) {
+		st->frame = 0;
+		maxScrollBitmap(bImgCurrent, 4, 1);
+		maxScrollBitmap(bImgTarget, 4, 1);
+		for(int i=0; i<8; i++) bImgCurrent[i*4+3] &= 0xFE;
+		busy = drawScroll(bImgTarget, st);
+
+		if(++(st->offset) >= st->width+32 || st->offset >= SCROLL_BUF_SIZE) {
+			st->offset = 0;
+		}
+	} else if(st->width > 32) {
+		st->frame += 1;
+		busy = 2;
+	} else {
+		st->offset = 0;
+		for(int i=0; i<8; i++) {
+			memcpy(&bImgTarget[i*4], &(st->bImg)[i*SCROLL_BUF_SIZE/8], 4);
+		}
+	}
+	return busy;
+}
+
+int main(int argc, char* argv[]) {
+	int rc;
+	time_t rawtime, oldTime = 0;
+	uint8_t bImgTarget[4*8], bImgCurrent[4*8];
+	uint8_t pixelPositions[4*8*8];
+	srand((unsigned) time(&rawtime));
+
+	// Initialize the library
+	// num controllers, BCD mode, SPI channel, GPIO pin number for CS
+	rc = maxInit(4, 0, 0, 22);
+	if (rc != 0)
+	{
+		printf("Problem initializing max7219\n");
+		return 0;
+	}
+	maxSetIntensity(0);
+
+	MQTTClient client;
+	MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
+	MQTTClient_create(&client, ADDRESS, CLIENTID,
+			MQTTCLIENT_PERSISTENCE_NONE, NULL);
+	conn_opts.keepAliveInterval = 20;
+	conn_opts.cleansession = 1;
+	MQTTClient_setCallbacks(client, NULL, connlost, msgarrvd, delivered);
+	if ((rc = MQTTClient_connect(client, &conn_opts)) != MQTTCLIENT_SUCCESS)
+	{
+		printf("Failed to connect, return code %d\n", rc);
+		exit(EXIT_FAILURE);
+	}
+	MQTTClient_subscribe(client, "Timer/#", QOS);
+	MQTTClient_subscribe(client, "Room/display", QOS);
+
+	uint32_t busy = 0;
+	while(1) {
+		if(!busy)
+			time(&rawtime);
+		if(rawtime - oldTime < 10) {
+			drawTime(bImgTarget, &rawtime);
+		} else if(rawtime - oldTime < 40) {
+			drawDate(bImgTarget, &rawtime);
+		} else if(rawtime - oldTime < 50) {
+			busy = moveScroll(bImgCurrent, bImgTarget, &stDisplay);
+		} else if(rawtime - oldTime < 60) {
+			busy = moveScroll(bImgCurrent, bImgTarget, &stWeather);
+		} else {
+			busy = 0;
+			oldTime = rawtime;
+		}
+
+		// find pixels to change
+		int pixelsToChange = 0;
+		for(int i=0; i<4*8; i++) {
+			if(bImgCurrent[i] != bImgTarget[i]) {
+				uint8_t diff = bImgCurrent[i] ^ bImgTarget[i];
+				// iterate bits
+				int p = 0;
+				while(diff) {
+					if(diff & 1) {
+						pixelPositions[pixelsToChange++] = i*8 + p;
+					}
+					p++;
+					diff >>= 1;
+				}
+			}
+		}
+		if(pixelsToChange) {
+			// shuffle list
+			// if(pixelsToChange > 1) {
+			// 	for(int i=0; i<pixelsToChange-1; i++) {
+			// 		int j = i + rand() / (RAND_MAX / (pixelsToChange - i) + 1);
+			// 		uint8_t t = pixelPositions[j];
+			// 		pixelPositions[j] = pixelPositions[i];
+			// 		pixelPositions[i] = t;
+			// 	}
+			// }
+
+			// animate change
+			for(int i=0; i<(busy?2:1); i++) {
+				int pos = pixelPositions[rand() % pixelsToChange];
+				uint8_t bit = bImgTarget[pos/8] & (1 << (pos%8));
+				if(bit)
+					bImgCurrent[pos/8] |= bit;
+				else
+					bImgCurrent[pos/8] &= ~(1 << (pos%8));
+			}
+		}
+		if(pixelsToChange || busy==1) {
+			if(argc > 1)
+				debugPrint(bImgCurrent, 4);
+			maxSendImage(bImgCurrent, 4, 1);
+		}
+
+		usleep(1000000 / 50);
+	}
+
+	// Quit library and free resources
+	maxShutdown();
+	MQTTClient_disconnect(client, 10000);
+	MQTTClient_destroy(&client);
+	return rc;
+} /* main() */

+ 329 - 0
main.py

@@ -0,0 +1,329 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+# #################################################################################### #
+# this file is deprecated and replaced by the c implementation for performance reasons #
+# #################################################################################### #
+
+import re
+import time
+import json
+
+from datetime import datetime, timedelta
+from PIL import Image
+
+import errno
+from socket import error as socket_error
+
+from luma.led_matrix.device import max7219
+from luma.core.interface.serial import spi, noop
+from luma.core.render import canvas
+from luma.core.virtual import viewport
+from luma.core.legacy import text, show_message, textsize
+from luma.core.sprite_system import framerate_regulator
+from luma.core.legacy.font import proportional, CP437_FONT, TINY_FONT, SINCLAIR_FONT, LCD_FONT
+
+#import context  # Ensures paho is in PYTHONPATH
+import paho.mqtt.client as mqtt
+
+# create matrix device
+serial = spi(port=0, device=0, gpio=noop())
+device = max7219(serial, cascaded=4, block_orientation=-90, rotate=0)
+print("Created device")
+
+umlauts = {
+	u'\xe4': u'ae', #CP437 \x84
+	u'\xf6': u'oe', #CP437 \x94
+	u'\xfc': u'ue', #CP437 \x81
+	u'\xc4': u'Ae', #CP437 \x8e
+	u'\xd6': u'Oe', #CP437 \x99
+	u'\xdc': u'Ue', #CP437 \x9a
+	u'\xdf': u'ss', #CP437 \xe1
+	u'\xb0': u'\x7f', # LCD °
+}
+
+# start demo
+txt = []
+status = ""
+c = 0
+sleep = 0
+bar = [50, 25]
+
+device.contrast(0)
+
+def on_message(mqttc, obj, msg):
+	global txt, c, bar, status, sleep
+		
+	if msg.topic == "Minecraft/log":
+		txt.append(msg.payload.decode("utf-8")[33:])
+		
+	elif msg.topic == "Room/display":
+		txt.append(msg.payload.decode("utf-8"))
+		
+	elif msg.topic == "Timer/weather":
+		w = json.loads(msg.payload)
+		txt.append(u"{} {}°C".format(w["detail"], w["tempc"]))
+		#txt.append(str(w["mintemp"]-273.15) + u'\x7fC - ' + str(w["maxtemp"]-273.15) + u'\x7fC')
+		txt.append(u"LF {}%".format(w["humidity"]))
+		txt.append(u"WLK {}%".format(w["clouds"]))
+		txt.append(u"{} m/s".format(w["windspeed"]))
+		
+	elif msg.topic == "Timer/forecast":
+		return
+		pass
+		#w = json.loads(str(msg.payload))
+		#print(msg.topic + " " + str(w))
+	elif msg.topic == "Timer/alert/1" and msg.payload != "-1":
+		txt.append("Wecker: " + msg.payload.decode("utf-8")[0:5])
+
+	elif msg.topic == "Timer/isDark":
+		c &= ~2
+		c |= (msg.payload == "0") * 2
+			
+	elif msg.topic == "Timer/isNight":
+		c &= ~1
+		c |= (msg.payload == "0")
+
+	elif msg.topic == "Relaybox/3":
+		sleep = 1 if msg.payload == "1" else 0
+
+	else:
+		print(msg.topic + " " + msg.payload.decode("utf-8"))
+	
+	#device.contrast(c * 32)
+	
+def on_log(mqttc, obj, level, string):
+	print(string)
+
+def on_connect(client, userdata, flags, rc):
+	print("Connected with result code "+str(rc))
+	mqttc.subscribe("Relaybox/3", 2)
+	mqttc.subscribe("Timer/#", 2)
+	mqttc.subscribe("Minecraft/log", 2)
+	mqttc.subscribe("Room/display", 2)
+
+mqttc = mqtt.Client()
+
+mqttc.on_message = on_message
+mqttc.on_connect = on_connect
+
+# Uncomment to enable debug messages
+#mqttc.on_log = on_log
+
+mqttc.connect_async("localhost", 1883, 60)
+
+mqttc.loop_start()
+
+
+pos = 0
+words = []
+t = datetime.now()
+
+class TimeDisplay:
+	def __init__(self, y):
+		self.y = y
+		self.current_y = 1
+		self.isAnimating = False
+		self.t = None
+		self.draw = None
+
+	def helper(self):
+		hours = self.t.strftime('%H')
+		minutes = self.t.strftime('%M')
+		if hours[0] == "0":
+			text(self.draw, (7, self.y+1), hours[1], fill="white", font=LCD_FONT)
+		else:
+			text(self.draw, (0, self.y+1), hours, fill="white", font=LCD_FONT)
+		if self.t.second % 2:
+			text(self.draw, (15, self.y+1), ":", fill="white", font=proportional(TINY_FONT))
+		text(self.draw, (17, self.y+self.current_y), minutes, fill="white", font=LCD_FONT)
+
+	def show(self, draw, t, currentLine):
+		self.t = t
+		self.draw = draw
+		if t.second == 59 and not self.isAnimating and currentLine == self.y:
+			self.isAnimating = True
+			self.current_y = 1
+		
+		if self.isAnimating:
+			self.helper()
+			minutes = (self.t + timedelta(0,1)).strftime('%M')
+			text(self.draw, (17, self.y+self.current_y-8), minutes, fill="white", font=LCD_FONT)
+			self.current_y += 1
+			if self.current_y > 9:
+				self.current_y = 1
+				self.isAnimating = False
+				return False
+			return True
+
+		elif currentLine > self.y-8 and currentLine < self.y+8:
+			self.helper()
+
+		return False
+
+class DateDisplay:
+	def __init__(self, y):
+		self.y = y
+
+	def show(self, draw, t, currentLine):
+		if currentLine > self.y-8 and currentLine < self.y+8:
+			day = t.strftime("%d")
+			month = t.strftime("%m")
+			text(draw, (0, self.y+1), day, fill="white", font=LCD_FONT)
+			text(draw, (14, self.y+2), ".", fill="white", font=proportional(TINY_FONT))
+			text(draw, (16, self.y+1), month, fill="white", font=LCD_FONT)
+			text(draw, (31, self.y+2), ".", fill="white", font=proportional(TINY_FONT))
+		return False
+
+class ScrollText:
+	def __init__(self, y):
+		self.font = proportional(LCD_FONT)
+		self.y = y
+		self.draw = None
+		self.setText("LED Matrix")
+
+	def helper(self, x, chars):
+		text(self.draw, (x, self.y), chars, fill="white", font=self.font)
+
+	def show(self, draw, t, currentLine):
+		self.draw = draw
+
+		if currentLine <= self.y-8 or currentLine >= self.y+8:
+			return False
+
+		self.helper(self.x + self.headLen, self.txt[self.charIndex:self.charIndex+8])
+
+		if self.len >= 32:
+			self.x -= 1
+		else:
+			self.x = 0
+
+		if self.x + self.headLen <= -self.currentCharWidth and self.charIndex + 1 < len(self.txt):
+			self.charIndex += 1
+			self.headLen += self.currentCharWidth
+			self.currentCharWidth, _ = textsize(self.txt[self.charIndex], self.font)
+
+		if self.x <= -self.len:
+			self.x = 31
+			self.currentCharWidth, _ = textsize(self.txt[0], self.font)
+			self.charIndex = 0
+			self.headLen = 0
+		if self.x == 0:
+			return False
+			
+		return True
+		
+
+	def setText(self, txt):
+		self.len, _ = textsize(txt, self.font)
+		self.txt = txt + "      "
+		self.x = 0
+		self.currentCharWidth, _ = textsize(self.txt[0], self.font)
+		self.charIndex = 0
+		self.headLen = 0
+
+class NightDisplay:
+	def __init__(self, y):
+		self.y = y
+		self.t = None
+		self.draw = None
+
+	def helper(self):
+		hours = self.t.strftime('%H')
+		minutes = self.t.strftime('%M')
+				
+		for i in range(6):
+			if self.t.hour & (2**i):
+				self.draw.point((28 - i*5, self.y), fill="white")
+			if self.t.minute & (2**i):
+				self.draw.point((28 - i*5, self.y+3), fill="white")
+			if self.t.second & (2**i):
+				self.draw.point((28 - i*5, self.y+6), fill="white")
+
+
+	def show(self, draw, t, currentLine):
+		self.t = t
+		self.draw = draw
+		if currentLine <= self.y-8 or currentLine >= self.y+8:
+			return False
+		
+		self.helper()
+
+		return False
+
+lines = [
+	(TimeDisplay(0), 1),
+	(DateDisplay(8), 30),
+	(ScrollText(16), 20),
+	(TimeDisplay(24), 10),
+	(NightDisplay(32), 5)
+]
+
+virtual = viewport(device, width=device.width, height=device.height*len(lines), mode="1")
+
+currentLine = 0
+targetLine = 0
+
+regulator = framerate_regulator(30)
+
+nextElem = datetime.now()
+
+try:
+	while True:
+		#if len(txt) == 0:
+		if datetime.now().second != t.second:
+			t = datetime.now()
+
+			
+			i = int(targetLine/8)
+			line, secs = lines[i]
+			if t >= nextElem:
+				targetLine = (targetLine+8) % 32
+				if sleep == 1:
+					targetLine = 32
+				i = int(targetLine/8)
+				line, secs = lines[i]
+				print("{} line={} time={}".format(t.strftime('%H:%M:%S'), i, secs))
+				nextElem = t + timedelta(0,secs)
+				if i == 2 and len(txt) > 0:
+					msg = u" +++ ".join(txt)
+					txt = []
+					print(msg.encode('utf-8'))
+
+					if len(msg) > 0:
+						msg = list(msg)
+
+						for i in range(len(msg)):
+							
+							if msg[i] in umlauts:
+								msg[i] = umlauts[msg[i]]
+						line.setText("".join(msg))
+
+			while True:
+				animate = True
+				if currentLine == 24 and targetLine < 12:
+					currentLine = 0
+				elif currentLine == 0 and targetLine > 12:
+					currentLine = 24
+				elif currentLine < targetLine:
+					currentLine += 1
+				elif currentLine > targetLine:
+					currentLine -= 1
+				else:
+					animate = False
+
+				virtual._position = (0, currentLine)
+
+				with regulator:
+					with canvas(virtual) as draw:
+						for line, _ in lines:
+							animate |= line.show(draw, t, currentLine)
+
+				if not animate:
+					break
+			
+		else:
+			time.sleep(1.0 - datetime.now().microsecond/1000000.0)
+
+except KeyboardInterrupt:
+	pass

+ 470 - 0
max7219.c

@@ -0,0 +1,470 @@
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdint.h>
+#include <armbianio.h>
+#include "max7219.h"
+//
+// MAX7219 LED Matrix controller library
+// Copyright (c) 2018 BitBank Software, Inc.
+// Written by Larry Bank
+// bitbank@pobox.com
+// Project started 3/10/2018
+//
+// 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 <http://www.gnu.org/licenses/>.
+//
+// I purchased a 4-module unit of red 8x8 LED matrices. They're wired such that the leftmost module is the last
+// in the chain and the bits are arranged such that the MSB is on the left and row 0 is the top row.
+//
+// This library contains a basic set of functions to initialize the modules, set the intensity, draw text in 2 font
+// sizes and scroll a bitmap (which can contain text or any graphics you put there
+//
+// The modules are connected to the SPI clock and data lines as well as a digital output pin to control the latching
+// of the data. The SPI CS line is not sufficient to properly control the loading of the data. Each module expects
+// to receive 16-bits of data at a time with D15 sent first and D0 sent last. The data contains an instruction in
+// D11-D8 and data for that instruction in D7-D0. In my case of 4 modules cascaded together, a data stream of
+// 16 x 4 = 64 bits must be transmitted at a time to latch data to all modules simultaneously. The controllers
+// randomly power up in test mode (all LEDs on at their brightest). For this reason, the init function disables
+// test mode just in case.
+//
+// Each module shifts bits in and the next bit is shifted out. For example, in the case of my setup with 4 modules
+  const uint8_t ucFont[] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x7e,0x81,0x95,0xb1,0xb1,0x95,0x81,0x7e,
+  0x7e,0xff,0xeb,0xcf,0xcf,0xeb,0xff,0x7e,0x0e,0x1f,0x3f,0x7e,0x3f,0x1f,0x0e,0x00,
+  0x08,0x1c,0x3e,0x7f,0x3e,0x1c,0x08,0x00,0x38,0x9a,0x9f,0xff,0x9f,0x9a,0x38,0x00,
+  0x10,0xb8,0xfc,0xfe,0xfc,0xb8,0x10,0x00,0x00,0x00,0x18,0x3c,0x3c,0x18,0x00,0x00,
+  0xff,0xff,0xe7,0xc3,0xc3,0xe7,0xff,0xff,0x00,0x3c,0x66,0x42,0x42,0x66,0x3c,0x00,
+  0xff,0xc3,0x99,0xbd,0xbd,0x99,0xc3,0xff,0x70,0xf8,0x88,0x88,0xfd,0x7f,0x07,0x0f,
+  0x00,0x4e,0x5f,0xf1,0xf1,0x5f,0x4e,0x00,0xc0,0xe0,0xff,0x7f,0x05,0x05,0x07,0x07,
+  0xc0,0xff,0x7f,0x05,0x05,0x65,0x7f,0x3f,0x99,0x5a,0x3c,0xe7,0xe7,0x3c,0x5a,0x99,
+  0x7f,0x3e,0x3e,0x1c,0x1c,0x08,0x08,0x00,0x08,0x08,0x1c,0x1c,0x3e,0x3e,0x7f,0x00,
+  0x00,0x24,0x66,0xff,0xff,0x66,0x24,0x00,0x00,0x5f,0x5f,0x00,0x00,0x5f,0x5f,0x00,
+  0x06,0x0f,0x09,0x7f,0x7f,0x01,0x7f,0x7f,0xc0,0x9a,0xbf,0xa5,0xbd,0xd9,0x43,0x02,
+  0x00,0x70,0x70,0x70,0x70,0x70,0x70,0x00,0x80,0x94,0xb6,0xff,0xff,0xb6,0x94,0x80,
+  0x00,0x04,0x06,0x7f,0x7f,0x06,0x04,0x00,0x00,0x10,0x30,0x7f,0x7f,0x30,0x10,0x00,
+  0x08,0x08,0x08,0x2a,0x3e,0x1c,0x08,0x00,0x08,0x1c,0x3e,0x2a,0x08,0x08,0x08,0x00,
+  0x3c,0x3c,0x20,0x20,0x20,0x20,0x20,0x00,0x08,0x1c,0x3e,0x08,0x08,0x3e,0x1c,0x08,
+  0x30,0x38,0x3c,0x3e,0x3e,0x3c,0x38,0x30,0x06,0x0e,0x1e,0x3e,0x3e,0x1e,0x0e,0x06,
+  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x5f,0x5f,0x06,0x00,0x00,
+  0x00,0x07,0x07,0x00,0x07,0x07,0x00,0x00,0x14,0x7f,0x7f,0x14,0x7f,0x7f,0x14,0x00,
+  0x24,0x2e,0x2a,0x6b,0x6b,0x3a,0x12,0x00,0x46,0x66,0x30,0x18,0x0c,0x66,0x62,0x00,
+  0x30,0x7a,0x4f,0x5d,0x37,0x7a,0x48,0x00,0x00,0x04,0x07,0x03,0x00,0x00,0x00,0x00,
+  0x00,0x1c,0x3e,0x63,0x41,0x00,0x00,0x00,0x00,0x41,0x63,0x3e,0x1c,0x00,0x00,0x00,
+  0x08,0x2a,0x3e,0x1c,0x1c,0x3e,0x2a,0x08,0x00,0x08,0x08,0x3e,0x3e,0x08,0x08,0x00,
+  0x00,0x00,0x80,0xe0,0x60,0x00,0x00,0x00,0x00,0x08,0x08,0x08,0x08,0x08,0x08,0x00,
+  0x00,0x00,0x00,0x60,0x60,0x00,0x00,0x00,0x60,0x30,0x18,0x0c,0x06,0x03,0x01,0x00,
+  0x3e,0x7f,0x59,0x4d,0x47,0x7f,0x3e,0x00,0x40,0x42,0x7f,0x7f,0x40,0x40,0x00,0x00,
+  0x62,0x73,0x59,0x49,0x6f,0x66,0x00,0x00,0x22,0x63,0x49,0x49,0x7f,0x36,0x00,0x00,
+  0x18,0x1c,0x16,0x53,0x7f,0x7f,0x50,0x00,0x27,0x67,0x45,0x45,0x7d,0x39,0x00,0x00,
+  0x3c,0x7e,0x4b,0x49,0x79,0x30,0x00,0x00,0x03,0x03,0x71,0x79,0x0f,0x07,0x00,0x00,
+  0x36,0x7f,0x49,0x49,0x7f,0x36,0x00,0x00,0x06,0x4f,0x49,0x69,0x3f,0x1e,0x00,0x00,
+  0x00,0x00,0x00,0x66,0x66,0x00,0x00,0x00,0x00,0x00,0x80,0xe6,0x66,0x00,0x00,0x00,
+  0x08,0x1c,0x36,0x63,0x41,0x00,0x00,0x00,0x00,0x14,0x14,0x14,0x14,0x14,0x14,0x00,
+  0x00,0x41,0x63,0x36,0x1c,0x08,0x00,0x00,0x00,0x02,0x03,0x59,0x5d,0x07,0x02,0x00,
+  0x3e,0x7f,0x41,0x5d,0x5d,0x5f,0x0e,0x00,0x7c,0x7e,0x13,0x13,0x7e,0x7c,0x00,0x00,
+  0x41,0x7f,0x7f,0x49,0x49,0x7f,0x36,0x00,0x1c,0x3e,0x63,0x41,0x41,0x63,0x22,0x00,
+  0x41,0x7f,0x7f,0x41,0x63,0x3e,0x1c,0x00,0x41,0x7f,0x7f,0x49,0x5d,0x41,0x63,0x00,
+  0x41,0x7f,0x7f,0x49,0x1d,0x01,0x03,0x00,0x1c,0x3e,0x63,0x41,0x51,0x33,0x72,0x00,
+  0x7f,0x7f,0x08,0x08,0x7f,0x7f,0x00,0x00,0x00,0x41,0x7f,0x7f,0x41,0x00,0x00,0x00,
+  0x30,0x70,0x40,0x41,0x7f,0x3f,0x01,0x00,0x41,0x7f,0x7f,0x08,0x1c,0x77,0x63,0x00,
+  0x41,0x7f,0x7f,0x41,0x40,0x60,0x70,0x00,0x7f,0x7f,0x0e,0x1c,0x0e,0x7f,0x7f,0x00,
+  0x7f,0x7f,0x06,0x0c,0x18,0x7f,0x7f,0x00,0x1c,0x3e,0x63,0x41,0x63,0x3e,0x1c,0x00,
+  0x41,0x7f,0x7f,0x49,0x09,0x0f,0x06,0x00,0x1e,0x3f,0x21,0x31,0x61,0x7f,0x5e,0x00,
+  0x41,0x7f,0x7f,0x09,0x19,0x7f,0x66,0x00,0x26,0x6f,0x4d,0x49,0x59,0x73,0x32,0x00,
+  0x03,0x41,0x7f,0x7f,0x41,0x03,0x00,0x00,0x7f,0x7f,0x40,0x40,0x7f,0x7f,0x00,0x00,
+  0x1f,0x3f,0x60,0x60,0x3f,0x1f,0x00,0x00,0x3f,0x7f,0x60,0x30,0x60,0x7f,0x3f,0x00,
+  0x63,0x77,0x1c,0x08,0x1c,0x77,0x63,0x00,0x07,0x4f,0x78,0x78,0x4f,0x07,0x00,0x00,
+  0x47,0x63,0x71,0x59,0x4d,0x67,0x73,0x00,0x00,0x7f,0x7f,0x41,0x41,0x00,0x00,0x00,
+  0x01,0x03,0x06,0x0c,0x18,0x30,0x60,0x00,0x00,0x41,0x41,0x7f,0x7f,0x00,0x00,0x00,
+  0x08,0x0c,0x06,0x03,0x06,0x0c,0x08,0x00,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,
+  0x00,0x00,0x03,0x07,0x04,0x00,0x00,0x00,0x20,0x74,0x54,0x54,0x3c,0x78,0x40,0x00,
+  0x41,0x7f,0x3f,0x48,0x48,0x78,0x30,0x00,0x38,0x7c,0x44,0x44,0x6c,0x28,0x00,0x00,
+  0x30,0x78,0x48,0x49,0x3f,0x7f,0x40,0x00,0x38,0x7c,0x54,0x54,0x5c,0x18,0x00,0x00,
+  0x48,0x7e,0x7f,0x49,0x03,0x06,0x00,0x00,0x98,0xbc,0xa4,0xa4,0xf8,0x7c,0x04,0x00,
+  0x41,0x7f,0x7f,0x08,0x04,0x7c,0x78,0x00,0x00,0x44,0x7d,0x7d,0x40,0x00,0x00,0x00,
+  0x60,0xe0,0x80,0x84,0xfd,0x7d,0x00,0x00,0x41,0x7f,0x7f,0x10,0x38,0x6c,0x44,0x00,
+  0x00,0x41,0x7f,0x7f,0x40,0x00,0x00,0x00,0x7c,0x7c,0x18,0x78,0x1c,0x7c,0x78,0x00,
+  0x7c,0x78,0x04,0x04,0x7c,0x78,0x00,0x00,0x38,0x7c,0x44,0x44,0x7c,0x38,0x00,0x00,
+  0x84,0xfc,0xf8,0xa4,0x24,0x3c,0x18,0x00,0x18,0x3c,0x24,0xa4,0xf8,0xfc,0x84,0x00,
+  0x44,0x7c,0x78,0x4c,0x04,0x0c,0x18,0x00,0x48,0x5c,0x54,0x74,0x64,0x24,0x00,0x00,
+  0x04,0x04,0x3e,0x7f,0x44,0x24,0x00,0x00,0x3c,0x7c,0x40,0x40,0x3c,0x7c,0x40,0x00,
+  0x1c,0x3c,0x60,0x60,0x3c,0x1c,0x00,0x00,0x3c,0x7c,0x60,0x30,0x60,0x7c,0x3c,0x00,
+  0x44,0x6c,0x38,0x10,0x38,0x6c,0x44,0x00,0x9c,0xbc,0xa0,0xa0,0xfc,0x7c,0x00,0x00,
+  0x4c,0x64,0x74,0x5c,0x4c,0x64,0x00,0x00,0x08,0x08,0x3e,0x77,0x41,0x41,0x00,0x00,
+  0x00,0x00,0x00,0x77,0x77,0x00,0x00,0x00,0x41,0x41,0x77,0x3e,0x08,0x08,0x00,0x00,
+  0x02,0x03,0x01,0x03,0x02,0x03,0x01,0x00,0x70,0x78,0x4c,0x46,0x4c,0x78,0x70,0x00};
+
+// 5x7 font (in 6x8 cell)
+const uint8_t ucSmallFont[] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3e,0x45,0x51,0x45,0x3e,0x00,0x3e,0x6b,0x6f,
+  0x6b,0x3e,0x00,0x1c,0x3e,0x7c,0x3e,0x1c,0x00,0x18,0x3c,0x7e,0x3c,0x18,0x00,0x30,
+  0x36,0x7f,0x36,0x30,0x00,0x18,0x5c,0x7e,0x5c,0x18,0x00,0x00,0x18,0x18,0x00,0x00,
+  0x00,0xff,0xe7,0xe7,0xff,0xff,0x00,0x3c,0x24,0x24,0x3c,0x00,0x00,0xc3,0xdb,0xdb,
+  0xc3,0xff,0x00,0x30,0x48,0x4a,0x36,0x0e,0x00,0x06,0x29,0x79,0x29,0x06,0x00,0x60,
+  0x70,0x3f,0x02,0x04,0x00,0x60,0x7e,0x0a,0x35,0x3f,0x00,0x2a,0x1c,0x36,0x1c,0x2a,
+  0x00,0x00,0x7f,0x3e,0x1c,0x08,0x00,0x08,0x1c,0x3e,0x7f,0x00,0x00,0x14,0x36,0x7f,
+  0x36,0x14,0x00,0x00,0x5f,0x00,0x5f,0x00,0x00,0x06,0x09,0x7f,0x01,0x7f,0x00,0x22,
+  0x4d,0x55,0x59,0x22,0x00,0x60,0x60,0x60,0x60,0x00,0x00,0x14,0xb6,0xff,0xb6,0x14,
+  0x00,0x04,0x06,0x7f,0x06,0x04,0x00,0x10,0x30,0x7f,0x30,0x10,0x00,0x08,0x08,0x3e,
+  0x1c,0x08,0x00,0x08,0x1c,0x3e,0x08,0x08,0x00,0x78,0x40,0x40,0x40,0x40,0x00,0x08,
+  0x3e,0x08,0x3e,0x08,0x00,0x30,0x3c,0x3f,0x3c,0x30,0x00,0x03,0x0f,0x3f,0x0f,0x03,
+  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x5f,0x06,0x00,0x00,0x07,0x03,0x00,
+  0x07,0x03,0x00,0x24,0x7e,0x24,0x7e,0x24,0x00,0x24,0x2b,0x6a,0x12,0x00,0x00,0x63,
+  0x13,0x08,0x64,0x63,0x00,0x36,0x49,0x56,0x20,0x50,0x00,0x00,0x07,0x03,0x00,0x00,
+  0x00,0x00,0x3e,0x41,0x00,0x00,0x00,0x00,0x41,0x3e,0x00,0x00,0x00,0x08,0x3e,0x1c,
+  0x3e,0x08,0x00,0x08,0x08,0x3e,0x08,0x08,0x00,0x00,0xe0,0x60,0x00,0x00,0x00,0x08,
+  0x08,0x08,0x08,0x08,0x00,0x00,0x60,0x60,0x00,0x00,0x00,0x20,0x10,0x08,0x04,0x02,
+  0x00,0x3e,0x51,0x49,0x45,0x3e,0x00,0x00,0x42,0x7f,0x40,0x00,0x00,0x62,0x51,0x49,
+  0x49,0x46,0x00,0x22,0x49,0x49,0x49,0x36,0x00,0x18,0x14,0x12,0x7f,0x10,0x00,0x2f,
+  0x49,0x49,0x49,0x31,0x00,0x3c,0x4a,0x49,0x49,0x30,0x00,0x01,0x71,0x09,0x05,0x03,
+  0x00,0x36,0x49,0x49,0x49,0x36,0x00,0x06,0x49,0x49,0x29,0x1e,0x00,0x00,0x6c,0x6c,
+  0x00,0x00,0x00,0x00,0xec,0x6c,0x00,0x00,0x00,0x08,0x14,0x22,0x41,0x00,0x00,0x24,
+  0x24,0x24,0x24,0x24,0x00,0x00,0x41,0x22,0x14,0x08,0x00,0x02,0x01,0x59,0x09,0x06,
+  0x00,0x3e,0x41,0x5d,0x55,0x1e,0x00,0x7e,0x11,0x11,0x11,0x7e,0x00,0x7f,0x49,0x49,
+  0x49,0x36,0x00,0x3e,0x41,0x41,0x41,0x22,0x00,0x7f,0x41,0x41,0x41,0x3e,0x00,0x7f,
+  0x49,0x49,0x49,0x41,0x00,0x7f,0x09,0x09,0x09,0x01,0x00,0x3e,0x41,0x49,0x49,0x7a,
+  0x00,0x7f,0x08,0x08,0x08,0x7f,0x00,0x00,0x41,0x7f,0x41,0x00,0x00,0x30,0x40,0x40,
+  0x40,0x3f,0x00,0x7f,0x08,0x14,0x22,0x41,0x00,0x7f,0x40,0x40,0x40,0x40,0x00,0x7f,
+  0x02,0x04,0x02,0x7f,0x00,0x7f,0x02,0x04,0x08,0x7f,0x00,0x3e,0x41,0x41,0x41,0x3e,
+  0x00,0x7f,0x09,0x09,0x09,0x06,0x00,0x3e,0x41,0x51,0x21,0x5e,0x00,0x7f,0x09,0x09,
+  0x19,0x66,0x00,0x26,0x49,0x49,0x49,0x32,0x00,0x01,0x01,0x7f,0x01,0x01,0x00,0x3f,
+  0x40,0x40,0x40,0x3f,0x00,0x1f,0x20,0x40,0x20,0x1f,0x00,0x3f,0x40,0x3c,0x40,0x3f,
+  0x00,0x63,0x14,0x08,0x14,0x63,0x00,0x07,0x08,0x70,0x08,0x07,0x00,0x71,0x49,0x45,
+  0x43,0x00,0x00,0x00,0x7f,0x41,0x41,0x00,0x00,0x02,0x04,0x08,0x10,0x20,0x00,0x00,
+  0x41,0x41,0x7f,0x00,0x00,0x04,0x02,0x01,0x02,0x04,0x00,0x80,0x80,0x80,0x80,0x80,
+  0x00,0x00,0x03,0x07,0x00,0x00,0x00,0x20,0x54,0x54,0x54,0x78,0x00,0x7f,0x44,0x44,
+  0x44,0x38,0x00,0x38,0x44,0x44,0x44,0x28,0x00,0x38,0x44,0x44,0x44,0x7f,0x00,0x38,
+  0x54,0x54,0x54,0x08,0x00,0x08,0x7e,0x09,0x09,0x00,0x00,0x18,0xa4,0xa4,0xa4,0x7c,
+  0x00,0x7f,0x04,0x04,0x78,0x00,0x00,0x00,0x00,0x7d,0x40,0x00,0x00,0x40,0x80,0x84,
+  0x7d,0x00,0x00,0x7f,0x10,0x28,0x44,0x00,0x00,0x00,0x00,0x7f,0x40,0x00,0x00,0x7c,
+  0x04,0x18,0x04,0x78,0x00,0x7c,0x04,0x04,0x78,0x00,0x00,0x38,0x44,0x44,0x44,0x38,
+  0x00,0xfc,0x44,0x44,0x44,0x38,0x00,0x38,0x44,0x44,0x44,0xfc,0x00,0x44,0x78,0x44,
+  0x04,0x08,0x00,0x08,0x54,0x54,0x54,0x20,0x00,0x04,0x3e,0x44,0x24,0x00,0x00,0x3c,
+  0x40,0x20,0x7c,0x00,0x00,0x1c,0x20,0x40,0x20,0x1c,0x00,0x3c,0x60,0x30,0x60,0x3c,
+  0x00,0x6c,0x10,0x10,0x6c,0x00,0x00,0x9c,0xa0,0x60,0x3c,0x00,0x00,0x64,0x54,0x54,
+  0x4c,0x00,0x00,0x08,0x3e,0x41,0x41,0x00,0x00,0x00,0x00,0x77,0x00,0x00,0x00,0x00,
+  0x41,0x41,0x3e,0x08,0x00,0x02,0x01,0x02,0x01,0x00,0x00,0x3c,0x26,0x23,0x26,0x3c};
+
+static uint8_t iNumControllers, iCSPin;
+static int file_spi = -1;
+
+//
+// Transmit a sequence of N x 16 bits to the cascaded controllers
+//
+// Send an atomic sequence of uint8_ts for loading all chained controllers (16 bits per controller)
+// Each controller acts like a 16-bit shift register and passes on the bits to the next controller. The
+// last 16-bits to sit in the controller will be latched when the CS line goes high.
+// uint8_t 0 -> D15-D8 (XXXX A3 A2 A1 A0), 4-bits unused and 4-bit register address
+// uint8_t 1 -> D7-D0 (8-bit data)
+//
+void maxSendSequence(uint8_t *pSequence, uint8_t len)
+{
+  // The CS line stays low throughout a "transaction". Send all of the control uint8_ts for all of the chained
+  // controllers in a single transaction. When the CS line rises, the data will be latched
+   AIOWriteGPIO(iCSPin, 0);
+   AIOWriteSPI(file_spi, pSequence, len);
+   AIOWriteGPIO(iCSPin, 1);
+   
+} /* maxSendSequence() */
+
+//
+// Power on or off the LED controllers
+//
+void maxPowerUp(uint8_t bPowerUp)
+{
+uint8_t i;
+uint8_t *d, bTemp[32]; // up to 16 controllers
+  d = bTemp;
+  for (i=0; i<iNumControllers; i++)
+  {
+     *d++ = 0x0C; // power up/down
+     *d++ = bPowerUp;
+  }
+  maxSendSequence(bTemp, iNumControllers * 2); // send the power up/down instruction
+} /* maxPowerUp() */
+//
+// Set the intensity (duty cycle of PWM signal) for the LED segments
+// valid values are 0 (dimmest) to 15 (brightest)
+//
+void maxSetIntensity(uint8_t bIntensity)
+{
+uint8_t *d, bTemp[32];
+uint8_t i;
+
+  d = bTemp;
+  for (i=0; i<iNumControllers; i++)
+  {
+      *d++ = 0x0A; // set intensity
+      *d++ = bIntensity;
+  } // for i
+  maxSendSequence(bTemp, iNumControllers * 2);
+} /* maxSetIntensity() */
+//
+// Set the segment decode mode (BCD or none)
+//
+void maxSetSegmentMode(uint8_t bMode)
+{
+uint8_t i, *d, bTemp[32];
+
+  d = bTemp;
+  for (i=0; i<iNumControllers; i++)
+  {
+      *d++ = 0x09; // decode mode
+      *d++ = (bMode) ? 0xff : 0x00;
+  } // for i
+  maxSendSequence(bTemp, iNumControllers * 2); // send the scan limit instructions to all controllers
+} /* maxSetSegmentMode() */
+
+static unsigned char lookup[16] = {
+0x0, 0x8, 0x4, 0xc, 0x2, 0xa, 0x6, 0xe,
+0x1, 0x9, 0x5, 0xd, 0x3, 0xb, 0x7, 0xf, };
+
+uint8_t reverse(uint8_t n) {
+   // Reverse the top and bottom nibble then swap them.
+   return (lookup[n&0b1111] << 4) | lookup[n>>4];
+}
+
+//
+// Send image data to the array of controllers
+// The image data is transmitted as N by 8 lines tall (N is the number of MAX7219 controllers)
+// The pitch (uint8_ts per line) can be any value
+//
+void maxSendImage(uint8_t *pImage, int iPitch, uint8_t rotated)
+{
+uint8_t i, j;
+uint8_t *s, *d, bTemp[32];
+   if(rotated) {
+      for (j=0; j<8; j++) // 8 rows to transmit
+      {
+         s = &pImage[iPitch * (j+1) - 1];
+         d = bTemp;
+         for (i=0; i<iNumControllers; i++)
+         {
+            *d++ = (8-j); // row number is the "instruction"
+            *d++ = reverse(*s--); // image data
+         } // for each controller
+         maxSendSequence(bTemp, iNumControllers * 2);
+      } // for each row of image
+   } else {
+      for (j=0; j<8; j++) // 8 rows to transmit
+      {
+         s = &pImage[iPitch * j];
+         d = bTemp;
+         for (i=0; i<iNumControllers; i++)
+         {
+            *d++ = (j+1); // row number is the "instruction"
+            *d++ = *s++; // image data
+         } // for each controller
+         maxSendSequence(bTemp, iNumControllers * 2);
+      } // for each row of image
+   }
+} /* maxSendImage() */
+
+//
+// Enable (1) or disable (0) test mode
+// This mode lights up every LED at max brightness
+// It can sometimes power up in test mode
+//
+void maxSetTestMode(uint8_t bOn)
+{
+uint8_t i, *d, bTemp[32];
+
+  d = bTemp;
+  for (i=0; i<iNumControllers; i++)
+  {
+      *d++ = 0x0F; // test mode
+      *d++ = bOn;
+  } // for i
+  maxSendSequence(bTemp, iNumControllers * 2); // send the scan limit instructions to all controllers
+} /* maxSetTestMode() */
+//
+// Number of "digits/rows" to control
+// valid values are 1-8 active digits/rows
+//
+void maxSetLimit(uint8_t bLimit)
+{
+uint8_t i, *d, bTemp[32];
+
+  d = bTemp;
+  for (i=0; i<iNumControllers; i++)
+  {
+      *d++ = 0x0B; // set scan limit
+      *d++ = (bLimit - 1);
+  } // for i
+  maxSendSequence(bTemp, iNumControllers * 2); // send the scan limit instructions to all controllers
+} /* maxSetLimit() */
+
+//
+// Send an ASCII string of numbers/spaces/decimal points
+// to a 7-segment display
+//
+void maxSegmentString(char *pString)
+{
+unsigned char ucTemp[4];
+int iDigit;
+
+	memset(ucTemp, 0, sizeof(ucTemp));
+	iDigit = 0;
+	while (*pString && iDigit < 8)
+	{
+		ucTemp[0] = 8 - (iDigit & 7); // cmd byte to write
+		if (pString[0] >= '0' && pString[0] <= '9')
+		{
+			ucTemp[1] = *pString++; // store digit
+			if (pString[0] == '.')
+			{
+				ucTemp[1] |= 0x80; // turn on decimal point
+				pString++;
+			}
+		}
+		else
+		{
+			ucTemp[1] = 0xf; // space = all segments off
+			pString++;
+		}
+		iDigit++;
+		maxSendSequence(ucTemp, 2); // need to latch each byte pair
+	}
+	while (iDigit < 8) // blank out remaining digits
+	{
+		ucTemp[0] = 8 - (iDigit & 7);
+		ucTemp[1] = 0xf; // all segments off
+		iDigit++;
+		maxSendSequence(ucTemp, 2);
+	}
+} /* maxSegmentString() */
+//
+// Draw a string of characters into the image buffer
+// Normal characters are 8x8 and drawn on uint8_t boundaries
+// Small characters are 6x8 and drawn on bit boundaries
+//
+void maxDrawString(char *pString, uint8_t *pImage, uint8_t iPitch, uint8_t bSmall)
+{
+uint8_t b, bMask, i, j, *d, bCol;
+const uint8_t *pFont;
+const uint8_t *s;
+int iWidth;
+
+   d = pImage;
+   bCol = 0;
+   if (bSmall)
+   {
+      pFont = ucSmallFont;
+      iWidth = 6;
+   }
+   else
+   {
+      pFont = ucFont;
+      iWidth = 8;
+   }
+   while (*pString)
+   {
+      b = *pString++;
+      s = &pFont[(int)b * iWidth]; // 6 or 8 uint8_ts per character in ASCII order
+      for (i=0; i<iWidth; i++) // column
+      {
+         bMask = (0x80 >> (bCol & 7));
+         b = *s++; // current font uint8_t
+         for (j=0; j<8; j++) // bit number of source becomes destination row
+         {
+            if (b & 1) // start from LSB
+               d[j*iPitch] |= bMask;
+            else
+               d[j*iPitch] &= ~bMask;
+            b >>= 1; // shift down font uint8_t
+         } // for j
+         bCol++;
+         if ((bCol & 7) == 0) // next uint8_t
+            d++;
+      } // for i
+   } // while string
+} /* maxDrawString() */
+
+//
+// Scroll a bitmap N bits left (positive) or right (negative)
+// Valid scroll values are +1 to +7 and -1 to -7
+// A bitmap is assumed to be iPitch uint8_ts wide by 8 rows tall
+// Bits which scroll off one end are added back to the other end
+//
+void maxScrollBitmap(uint8_t *pBitmap, int iPitch, int iScroll)
+{
+uint8_t b, bEdge, *s;
+int col, row;
+
+  if (iScroll > 0) // scroll left
+  {
+     for (row=0; row<8; row++)
+     {
+        s = &pBitmap[row * iPitch];
+        bEdge = s[0] >> (8-iScroll);
+        for (col=0; col<iPitch; col++)
+        {
+           b = s[col] << iScroll;
+           b |= (col == iPitch-1) ? bEdge : (s[col+1] >> (8-iScroll));
+           s[col] = b;
+        } // for col
+     } // for row
+  }
+  else // scroll right
+  {
+     iScroll = 0 - iScroll; // make it a positive number
+     for (row=0; row<8; row++)
+     {
+        s = &pBitmap[row * iPitch];
+        bEdge = s[iPitch-1] << (8-iScroll);
+        for (col=iPitch-1; col>=0; col--)
+        {
+           b = s[col] >> iScroll;
+           b |= (col == 0) ? bEdge : (s[col-1] << (8-iScroll));
+           s[col] = b;
+        } // for col
+     } // for row    
+  }
+} /* maxScrollBitmap() */
+//
+// Initialize the controllers
+//
+int maxInit(uint8_t iNum, uint8_t bDecodeMode, uint8_t iChannel, uint8_t iSelect)
+{
+
+  if (!AIOInit())
+  {
+     fprintf(stderr, "Error initializing ArmbianIO library\n");
+     return -1;
+  }
+  iNumControllers = iNum;
+  iCSPin = iSelect; // header pin number used for select line
+  file_spi = AIOOpenSPI(iChannel, 2000000); // 2Mhz is a reasonable speed
+  if (file_spi == -1)
+  {
+     fprintf(stderr, "Failed to open the SPI bus\n");
+     file_spi = -1;
+     return -1;
+  }
+  AIOAddGPIO(iCSPin, GPIO_OUT);
+  AIOWriteGPIO(iCSPin, 1);
+
+  maxPowerUp(1); // turn on the LED controllers
+  maxSetLimit(8); // tell it to display 8 rows of 8 pixels
+  maxSetIntensity(0); // set the minimum intensity to start (1/32 duty cycle)
+  maxSetSegmentMode(bDecodeMode); // sets BCD (7-segment), or none (pixels
+  maxSetTestMode(0); // disable test mode (it can accidentally get set at power up) 
+  return 0;
+} /* maxInit() */
+
+void maxShutdown(void)
+{
+   maxPowerUp(0); // turn off the LED controllers
+   AIOCloseSPI(file_spi);
+   file_spi = -1;
+   AIOShutdown();
+} /* maxShutdown() */

+ 73 - 0
max7219.h

@@ -0,0 +1,73 @@
+#ifndef __MAX7219_LIBRARY__
+#define __MAX7219_LIBRARY__
+//
+// 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 <http://www.gnu.org/licenses/>.
+//
+//
+// Power on or off the LED controllers
+//
+void maxPowerUp(uint8_t bPowerUp);
+//
+// Set the intensity (duty cycle of PWM signal) for the LED segments
+// valid values are 0 (dimmest) to 15 (brightest)
+//
+void maxSetIntensity(uint8_t bIntensity);
+//
+// Set the segment decode mode (BCD or none)
+//
+void maxSetSegmentMode(uint8_t bMode);
+//
+// Send image data to the array of controllers
+// The image data is transmitted as N by 8 lines tall (N is the number of MAX7219 controllers)
+// The pitch (uint8_ts per line) can be any value
+//
+void maxSendImage(uint8_t *pImage, int iPitch, uint8_t rotated);
+//
+// Enable (1) or disable (0) test mode
+// This mode lights up every LED at max brightness
+// It can sometimes power up in test mode
+//
+void maxSetTestMode(uint8_t bOn);
+//
+// Number of "digits/rows" to control
+// valid values are 1-8 active digits/rows
+//
+void maxSetLimit(uint8_t bLimit);
+//
+// Send an ASCII string of digits to a 7-segment display
+//
+void maxSegmentString(char *pString);
+//
+// Draw a string of characters into the image buffer
+// Normal characters are 8x8 and drawn on uint8_t boundaries
+// Small characters are 6x8 and drawn on bit boundaries
+//
+void maxDrawString(char *pString, uint8_t *pImage, uint8_t iPitch, uint8_t bSmall);
+//
+// Scroll a bitmap N bits left (positive) or right (negative)
+// Valid scroll values are +1 to +7 and -1 to -7
+// A bitmap is assumed to be iPitch uint8_ts wide by 8 rows tall
+// Bits which scroll off one end are added back to the other end
+//
+void maxScrollBitmap(uint8_t *pBitmap, int iPitch, int iScroll);
+//
+// Initialize the controllers
+// returns 0 for success, -1 for failure
+//
+int maxInit(uint8_t iNum, uint8_t bDecodeMode, uint8_t iChannel, uint8_t iSelect);
+//
+// Turn off the LED controllers and free resources
+//
+void maxShutdown(void);
+#endif // __MAX7219_LIBRARY__

+ 6 - 0
run.sh

@@ -0,0 +1,6 @@
+#!/bin/bash
+
+#python /home/pi/matrix/main.py &> /home/pi/matrix/py.log
+python3 /home/pi/matrix/webcam.py
+
+/home/pi/matrix/build

+ 36 - 0
types.h

@@ -0,0 +1,36 @@
+#ifndef TYPES_H
+#define TYPES_H
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <unistd.h>
+#include <time.h>
+#include <math.h>
+
+#define SCROLL_BUF_SIZE (128*6/8 * 8)
+#define WEATHER_DATA_COUNT (40)
+
+struct weather_t {
+  int valid;
+  time_t dt;
+  float temp;
+  float clouds;
+  float wind;
+  float rain;
+  float snow;
+};
+
+struct scrolltext_t {
+  // occupied width in px
+  uint16_t width;
+  // draw offset in px
+  uint16_t offset;
+  // no of held frames
+  uint16_t frame;
+  // frames to display each offset
+  uint8_t frameCount[SCROLL_BUF_SIZE];
+
+  uint8_t bImg[SCROLL_BUF_SIZE];
+};
+#endif

+ 42 - 0
webcam.py

@@ -0,0 +1,42 @@
+offline = False
+try:
+  from luma.led_matrix.device import max7219
+  from luma.core.interface.serial import spi, noop
+  from luma.core.render import canvas
+except ModuleNotFoundError as e:
+  offline = True
+  import numpy as np
+
+from PIL import Image, ImageDraw
+import cv2
+
+cap = cv2.VideoCapture(-1)
+
+if not offline:
+  # create matrix device
+  serial = spi(port=0, device=0, gpio=noop())
+  device = max7219(serial, cascaded=4, block_orientation=-90, rotate=0)
+  print("Created device")
+
+while True:
+  success, image = cap.read()
+  if success:
+    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
+    width, h = image.shape
+    height = int(width/32*8)
+    offset = int(h/2-height/2)
+    image = image[offset:offset+height, :]
+    image = cv2.resize(image, (32,8), interpolation = cv2.INTER_AREA)
+    image = cv2.Canny(image, 0, 200)
+    im = Image.fromarray(image)
+    im = im.convert('1')
+    if offline:
+      image = np.asarray(im.convert("RGB"))
+      cv2.imshow("image", cv2.resize(image, (640,120), interpolation = cv2.INTER_NEAREST))
+      if cv2.waitKey(1) & 0xFF == ord('q'):
+        break
+    else:
+      with canvas(device, im) as draw:
+        pass
+  else:
+    exit(1)