I don't like commercial vendor-locked smart TV's to such a degree that I made myself a DIY solution.

Actually, the recipe is well-known and straightforward: take some computer, install any available media-center OS, like OSMC, LibreELEC or any other on your taste, and enjoy! If you choose Raspberry Pi as the computing board, you even get support for HDMI-CEC, so you can control the player with the TV remote. This is very rare, because HDMI-CEC is a proprietary technology and is licensed by just a few computing parts vendors.

But the story is about the enclosure this mediacenter was housed in. In a previous life it was a cable TV box. I simply threw out its guts (leaving built-in power supply), dremeled few holes for Rasbberry Pi interfaces, added an HDD, et voilà! Now I have a factory-looking set top box. Except the front panel. Now it's dark and all buttons are dead. But it can be fixed, since we have a whole lot of free GPIO pins on our computing board.
Usually, 7-segment displays are built on a shift registers, like 74HC595 to reduse the number of used controller pins. And panel keyboards use diode matrix for the same purpose. The first task is to reverse-engineer the pinout for both. In this case, the keyboard schematic is quite complicated: it uses strobe signals of 4-digit 7-segment display to read the state of 7 buttons occupying just 2 additional controller pins:

The pinout looked like this:
RPi pin wpi Panel pin
3v3 1 1 +3v3
gnd 9 2 gnd
gpio17 11 0 3 keys pullup high: menu power ok
gpio27 13 2 4 keys pullup high: up down left right
gpio22 15 3 5 dig4 low katode: up4
gpio10 19 12 6 dig3 low katode: left4 menu3
gpio9 21 13 7 dig2 low katode: down4 power3
gpio11 23 14 8 dig1 low katode: right4 ok3 5ms low 15ms high
gnd 25 9 gnd
gpio0 27 30 10 pin1 serial input before clock rise
gpio5 29 21 11 pin9 clear 1 pulse low before clock
gpio6 31 22 12 pin8 clock 8 pulses 500ns period
gnd 39 13 gnd
gpio13 33 23 14 IR outTo control the pins, I used the WiringPi library. First version of the program was written on python, but I wasn't impressed with performance. Imperfect timings were very visible on a strobe-driven display, making its brightless uneven and jittery. So, a new program was written in C. Don't judge the code too much, it is obviously a mess. But it compiles and works. The panel now shows the current time, plays simple anomations on boot or poweroff and key presses are translated to the Kodi by calling the kodi-send program.
#include <stdio.h>
#include <wiringPi.h>
#include <time.h>
#include <string.h>
#include <stdbool.h>
#include <stdlib.h>
#include <sys/timex.h>
static int dataPin = 12; //pin1 serial input before clock rise
static int clearPin = 13; //pin9 clear 1 pulse low before clock
static int clockPin = 14; //pin8 clock 8 pulses 500ns period
static int digits [4] = { 3, 2, 0, 7 };
static int keys1 = 8; //keys pullup high: dig3menu dig2power dig1ok
static int keys2 = 9; //keys pullup high: dig4up dig2down dig3left dig1right
bool dot = true;
bool showdot;
bool keypressed;
int cycles = 0;
int btncycles = 0;
static int digitdelay = 4; //4ms per digit for 60Hz refresh rate
static int dotLength = 30; //30*16 ~= 500ms
static int btndelay = 15;
unsigned int seg(char symbol) {
switch (symbol) {
case '0' : return 0xC0;
case '1' : return 0xF9;
case '2' : return 0xA4;
case '3' : return 0xB0;
case '4' : return 0x99;
case '5' : return 0x92;
case '6' : return 0x82;
case '7' : return 0xF8;
case '8' : return 0x80;
case '9' : return 0x90;
case 'a' : return 0x88;
case 'b' : return 0x83;
case 'c' : return 0xc6;
case 'd' : return 0xa1;
case 'e' : return 0x86;
case 'f' : return 0x8e;
case 'g' : return 0xC2;
case 'h' : return 0x8b;
case 'i' : return 0xfb;
case 'j' : return 0xe1;
case 'k' : return 0x8a;
case 'l' : return 0xc7;
case 'm' : return 0xaa;
case 'n' : return 0xab;
case 'o' : return 0xa3;
case 'p' : return 0x8c;
case 'q' : return 0x98;
case 'r' : return 0xaf;
case 's' : return 0x92;
case 't' : return 0x87;
case 'u' : return 0xc1;
case 'v' : return 0xe3;
case 'w' : return 0x95;
case 'x' : return 0x89;
case 'y' : return 0x91;
case 'z' : return 0xe4;
case '.' : return 0x7f;
case '!' : return 0x7d;
case ' ' : return 0xff;
}
}
int write(char symbol, bool dot)
{
int bits = seg(symbol);
if(dot) {
bits ^= 1 << 7;
}
digitalWrite (clearPin, 1);
digitalWrite (clearPin, 1);
for (unsigned int i = 8 ; i > 0 ; --i) {
int bit = (bits >> i-1) & 1;
digitalWrite (clockPin, 0);
digitalWrite (dataPin, bit);
digitalWrite (clockPin, 1);
}
}
int writebyte(unsigned char symbol)
{
digitalWrite (clearPin, 1);
digitalWrite (clearPin, 1);
for (unsigned int i = 8 ; i > 0 ; --i) {
int bit = (symbol >> i-1) & 1;
digitalWrite (clockPin, 0);
digitalWrite (dataPin, bit);
digitalWrite (clockPin, 1);
}
}
int animation(unsigned int cycles)
{
static int initdelay = 100;
for (unsigned int i = 0 ; i < cycles ; ++i) {
unsigned char byte = 0xfe;
writebyte(byte);
digitalWrite (digits[0], 0); delay (initdelay); digitalWrite (digits[0], 1);
digitalWrite (digits[1], 0); delay (initdelay); digitalWrite (digits[1], 1);
digitalWrite (digits[2], 0); delay (initdelay); digitalWrite (digits[2], 1);
digitalWrite (digits[3], 0); delay (initdelay);
byte = 0xfd; writebyte(byte); delay (initdelay);
byte = 0xfb; writebyte(byte); delay (initdelay);
byte = 0xf7; writebyte(byte); delay (initdelay);
digitalWrite (digits[3], 1); digitalWrite (digits[2], 0); delay (initdelay); digitalWrite (digits[2], 1);
digitalWrite (digits[1], 0); delay (initdelay); digitalWrite (digits[1], 1);
digitalWrite (digits[0], 0); delay (initdelay);
byte = 0xef; writebyte(byte); delay (initdelay);
byte = 0xdf; writebyte(byte); delay (initdelay);
}
}
int keypress(char * key)
{
keypressed = true;
if (btncycles == 0) {
char buffer[256];
if(snprintf(buffer, sizeof(buffer), "kodi-send %s & ", key)>=sizeof(buffer)) {
} else {
system(buffer);
}
}
btncycles++;
if (btncycles > btndelay) {
btncycles = 0;
}
}
int main (int argc, char *argv[])
{
wiringPiSetup ();
pinMode (dataPin, OUTPUT);
pinMode (clearPin, OUTPUT);
pinMode (clockPin, OUTPUT);
pinMode (keys1, INPUT);
pinMode (keys2, INPUT);
for (unsigned int i = 0 ; i < 8 ; ++i) {
digitalWrite (clockPin, 0);
digitalWrite (dataPin, 0);
digitalWrite (clockPin, 1);
}
for (unsigned int i = 0 ; i < 4 ; ++i) {
pinMode (digits[i], OUTPUT);
digitalWrite (digits[i], 1);
}
if( argc == 2 && 0 == strcmp(argv[1], "stop")) {
delay (500);
char byeString[4] = "bye!";
for (unsigned int b = 0 ; b < 100 ; ++b) {
for (unsigned int i = 0 ; i < 4 ; ++i) {
write(byeString[i], false);
digitalWrite (digits[i], 0);
delay (digitdelay);
digitalWrite (digits[i], 1);
}
}
animation(1);
for (unsigned int i = 0 ; i < 4 ; ++i) {
digitalWrite (digits[i], 1);
}
} else {
for (;;) {
struct timex timex_info = {};
timex_info.modes = 0; // explicitly don't adjust any time parameters
int ntp_result = ntp_adjtime(&timex_info);
if (!(ntp_result >= 0 && ntp_result != TIME_ERROR)) {
animation(1);
} else {
cycles++;
if(cycles > dotLength) {
cycles = 0;
dot = !dot;
}
time_t current_time;
struct tm * time_info;
char timeString[9]; // space for "HH:MM:SS\0"
time(¤t_time);
time_info = localtime(¤t_time);
strftime(timeString, sizeof(timeString), "%H%M", time_info);
keypressed = false;
for (unsigned int i = 0 ; i < 4 ; ++i) {
if (i == 1) {
showdot = dot;
} else {
showdot = false;
}
write(timeString[i], showdot);
digitalWrite (digits[i], 0);
delay (digitdelay);
if (digitalRead(keys1) == 0) {
if (i == 0) {
keypress("--action=\"Select\"");
} else if (i == 1) {
keypress("--action=\"PreviousMenu\" --action=\"Stop\"");
} else if (i == 2) {
keypress("--action=\"Menu\"");
}
}
if (digitalRead(keys2) == 0) {
if (i == 0) {
keypress("--action=\"Right\" --action=\"StepForward\"");
} else if (i == 1) {
keypress("--action=\"Down\"");
} else if (i == 2) {
keypress("--action=\"Left\" --action=\"StepBack\"");
} else if (i == 3) {
keypress("--action=\"Up\"");
}
}
digitalWrite (digits[i], 1);
}
if (!keypressed && btncycles > 0) {
btncycles = 0;
}
}
}
}
return 0 ;
}To launch the program, a new systemd service was created:
#place in /lib/systemd/system/panel.service
#sudo systemctl daemon-reload
#sudo systemctl enable panel.service
#sudo systemctl start panel
[Unit]
Description = Front panel clock and buttons
After = mediacenter.service
[Service]
Type = simple
ExecStart=/home/osmc/.local/bin/panel-kaon
ExecStop=killall panel-kaon
ExecStop=/home/osmc/.local/bin/panel-kaon stop
[Install]
WantedBy = multi-user.target