aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.rst6
-rw-r--r--firmware/main.c16
-rw-r--r--host/.gitignore5
-rw-r--r--host/Makefile12
-rwxr-xr-xhost/aus.py20
-rw-r--r--host/bdf.c (renamed from host/main.c)91
-rw-r--r--host/bdf.h11
-rw-r--r--host/color.h9
-rw-r--r--host/default.lines88
-rw-r--r--host/font.c13
-rw-r--r--host/font.h3
-rw-r--r--host/main.h10
-rw-r--r--host/matelight.py40
-rw-r--r--host/matelight.service10
-rw-r--r--host/net.h18
-rwxr-xr-xhost/server.py249
-rw-r--r--host/terminal.py19
-rw-r--r--host/usb.c161
-rw-r--r--host/usb.h27
19 files changed, 586 insertions, 222 deletions
diff --git a/README.rst b/README.rst
index b6412a7..376db26 100644
--- a/README.rst
+++ b/README.rst
@@ -31,13 +31,19 @@ Related Projects
----------------
* `A Python script that plays gifs over CRAP`_
* `An HTML5 CRAP emulator`_
+* `A CRAP client for node.js`_
* `Webcam streaming on Mate Light`_
* `A game programming framework for Mate Light`_
* `Snake for Mate Light`_
* `Game of Life for Mate Light`_
+* `Mate Light Android App`_
+* `Blinkenlights for Mate Light`_
+* `Postillon Newsticker for Matelight`_
.. _`A Python script that plays gifs over CRAP`: https://github.com/uwekamper/matelight-gifplayer
.. _`An HTML5 CRAP emulator`: https://github.com/sodoku/matelightemu
+.. _`A CRAP client for node.js`: https://github.com/sodoku/node-matelight
+.. _`Postillon Newsticker for Matelight`: https://gist.github.com/XenGi/9168633
.. _`Webcam streaming on Mate Light`: https://github.com/c-base/matetv
.. _`A game programming framework for Mate Light`: https://github.com/c-base/pymlgame
.. _`Snake for Mate Light`: https://github.com/c-base/pymlsnake
diff --git a/firmware/main.c b/firmware/main.c
index 4b325fa..cc5c458 100644
--- a/firmware/main.c
+++ b/firmware/main.c
@@ -40,17 +40,17 @@
#define CRATE_SIZE (CRATE_WIDTH*CRATE_HEIGHT)
#define BUS_SIZE (CRATES_PER_BUS*CRATE_SIZE*BYTES_PER_PIXEL)
unsigned const char const BOTTLE_MAP[CRATE_SIZE] = {
- 17, 16, 15, 14, 13,
- 18, 9, 10, 11, 12,
- 19, 8, 7, 6, 5,
- 0, 1, 2, 3, 4
+ 4, 3, 2, 1, 0,
+ 5, 6, 7, 8,19,
+ 12, 11, 10, 9,18,
+ 13, 14, 15,16,17
};
unsigned const char const FUCKED_UP_BOTTLE_MAP[CRATE_SIZE] = {
- 3, 4, 5, 6, 7,
- 2, 11, 10, 9, 8,
- 1, 12, 13, 14, 15,
- 0, 19, 18, 17, 16
+ 16,17, 18,19, 0,
+ 15,14, 13,12, 1,
+ 8, 9, 10,11, 2,
+ 7, 6, 5, 4, 3
};
unsigned const char const CRATE_MAP[CRATE_COUNT] = {
diff --git a/host/.gitignore b/host/.gitignore
new file mode 100644
index 0000000..1903c5d
--- /dev/null
+++ b/host/.gitignore
@@ -0,0 +1,5 @@
+__pycache__
+env
+*.so
+*.swo
+*.swp
diff --git a/host/Makefile b/host/Makefile
index a6b892f..85536f5 100644
--- a/host/Makefile
+++ b/host/Makefile
@@ -1,6 +1,12 @@
-all: main.c font.c font.h color.c color.h
- gcc -shared -fPIC -std=gnu11 -Wall -lm -o libbdf.so -g -O0 main.c font.c color.c
+all: libml libbdf
+
+
+libml: usb.c color.c color.h
+ gcc -shared -fPIC -std=gnu11 -Wall -lm -lusb-1.0 -o libml.so -g usb.c color.c
+
+libbdf: bdf.c font.c font.h color.c color.h
+ gcc -shared -fPIC -std=gnu11 -Wall -lm -o libbdf.so -g bdf.c font.c color.c
clean:
- rm libbdf.so
+ rm libbdf.so libml.so
diff --git a/host/aus.py b/host/aus.py
new file mode 100755
index 0000000..c0bab71
--- /dev/null
+++ b/host/aus.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+
+from socketserver import *
+import socket
+import struct
+import zlib
+from time import time, strftime, sleep
+from collections import namedtuple, deque
+import itertools
+import threading
+import random
+import os
+
+from ctypes import *
+
+from matelight import sendframe, DISPLAY_WIDTH, DISPLAY_HEIGHT, FRAME_SIZE
+
+if __name__ == '__main__':
+ sendframe(bytes([0, 0, 0]*FRAME_SIZE))
+
diff --git a/host/main.c b/host/bdf.c
index 9648a26..a95ed8b 100644
--- a/host/main.c
+++ b/host/bdf.c
@@ -1,9 +1,8 @@
#include "config.h"
-#include "main.h"
+#include "bdf.h"
#include "color.h"
#include "font.h"
-#include "net.h"
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
@@ -18,20 +17,11 @@
#include <netinet/in.h>
#include <unistd.h>
-
-void free_framebuffer(framebuffer_t *fb){
- free(fb->data);
- free(fb);
-}
-
/* CAUTION: REQUIRES INPUT TO BE \0-TERMINATED
* ...also, it does a hardcoded setlocale of LC_CTYPE to en_US.utf8 for... reasons. */
-framebuffer_t *framebuffer_render_text(char *s, glyphtable_t *glyph_table){
- unsigned int len = strlen(s);
-
- color_t *gbuf = NULL;
- unsigned int gbufwidth = 0;
- unsigned int gbufheight = 0;
+int framebuffer_get_text_bounds(char *s, glyphtable_t *glyph_table, size_t *outw, size_t *outh){
+ size_t gbufwidth = 0;
+ size_t gbufheight = 0;
char *p = s;
/* Calculate screen width of string prior to allocating memory for the frame buffer */
@@ -65,7 +55,7 @@ framebuffer_t *framebuffer_render_text(char *s, glyphtable_t *glyph_table){
p += inc;
if(c > glyph_table->size){
- fprintf(stderr, "Error rendering string: Codepoint 0x%lx out of valid range (0-%ld).\n", (long int)c, glyph_table->size);
+ fprintf(stderr, "Error rendering string: Codepoint 0x%lx out of valid range (0-%zd).\n", (long int)c, glyph_table->size);
goto error;
}
@@ -81,19 +71,31 @@ framebuffer_t *framebuffer_render_text(char *s, glyphtable_t *glyph_table){
gbufwidth += g->width;
}
- /* For easier rendering on the terminal, round up to multiples of two */
- gbufheight += gbufheight&1;
+ *outw = gbufwidth;
+ *outh = gbufheight;
+ return 0;
+error:
+ return 1;
+}
+
+/* CAUTION: REQUIRES INPUT TO BE \0-TERMINATED
+ * ...also, it does a hardcoded setlocale of LC_CTYPE to en_US.utf8 for... reasons. */
+/* Render the string beginning from the specified x offset (in pixels) */
+int framebuffer_render_text(char *s, glyphtable_t *glyph_table, color_t *gbuf, size_t gbufwidth, size_t gbufheight, size_t offx){
+ unsigned int len = strlen(s);
+ char *p = s;
- size_t gbufsize = gbufwidth*gbufheight;
- gbuf = calloc(gbufsize, sizeof(color_t));
- if(!gbuf){
- fprintf(stderr, "Cannot malloc() %lu bytes.\n", gbufsize*sizeof(color_t));
+ if(!setlocale(LC_CTYPE, "en_US.utf8")){
+ fprintf(stderr, "Cannot set locale\n");
goto error;
}
- memset(gbuf, 0, gbufsize*sizeof(color_t));
+
+ memset(gbuf, 0, gbufwidth*gbufheight*sizeof(color_t));
unsigned int x = 0;
+ wchar_t c;
p = s;
+ mbstate_t ps = {0};
memset(&ps, 0, sizeof(mbstate_t));
struct {
color_t fg;
@@ -280,38 +282,35 @@ framebuffer_t *framebuffer_render_text(char *s, glyphtable_t *glyph_table){
color_t bg = inv ? style.bg : style.fg;
glyph_t *g = glyph_table->data[c];
- render_glyph(g, gbuf, gbufwidth, x, 0, fg, bg);
- if(style.strikethrough || style.underline){
- int sty = gbufheight/2;
- /* g->y usually is a negative index of the glyph's baseline measured from the glyph's bottom */
- int uly = gbufheight + g->y;
- for(int i=0; i<g->width; i++){
- if(style.strikethrough)
- gbuf[sty*gbufwidth + x + i] = fg;
- if(style.underline)
- gbuf[uly*gbufwidth + x + i] = fg;
+ /* Is the glyph within the buffer's bounds? */
+ //if(x+g->width > offx && x < offx+gbufwidth){
+ /* x-offx might be negative down to -g->width+1, but that's ok */
+ render_glyph(g, gbuf, gbufwidth, x-offx, 0, fg, bg);
+ if(style.strikethrough || style.underline){
+ int sty = gbufheight/2;
+ /* g->y usually is a negative index of the glyph's baseline measured from the glyph's bottom */
+ int uly = gbufheight + g->y;
+ for(int i=0; i<g->width; i++){
+ if(x+i >= offx && x+i-offx < gbufwidth){ /* Stay within the frame buffer's bounds */
+ if(style.strikethrough)
+ gbuf[sty*gbufwidth + x + i - offx] = fg;
+ if(style.underline)
+ gbuf[uly*gbufwidth + x + i - offx] = fg;
+ }
+ }
}
- }
+ //}
x += g->width;
}
- framebuffer_t *fb = malloc(sizeof(framebuffer_t));
- if(!fb){
- fprintf(stderr, "Cannot malloc() %lu bytes.\n", sizeof(framebuffer_t));
- goto error;
- }
- fb->w = gbufwidth;
- fb->h = gbufheight;
- fb->data = gbuf;
- return fb;
-error:
- free(gbuf);
return 0;
+error:
+ return 1;
}
void console_render_buffer(color_t *data, size_t w, size_t h){
/* Render framebuffer to terminal, two pixels per character using Unicode box drawing stuff */
color_t lastfg = {0, 0, 0}, lastbg = {0, 0, 0};
- printf("\e[38;5;0;48;5;0m");
+ printf("\e[38;5;0;48;5;0m\e[K");
for(size_t y=0; y < h; y+=2){
for(size_t x=0; x < w; x++){
/* Da magicks: ▀█▄ */
@@ -350,7 +349,7 @@ void console_render_buffer(color_t *data, size_t w, size_t h){
}
}
}
- printf("\n");
+ printf("\n\e[K");
}
printf("\033[0m");
}
diff --git a/host/bdf.h b/host/bdf.h
new file mode 100644
index 0000000..c81277e
--- /dev/null
+++ b/host/bdf.h
@@ -0,0 +1,11 @@
+#ifndef __MAIN_H__
+#define __MAIN_H__
+
+#include "color.h"
+#include "font.h"
+
+int framebuffer_get_text_bounds(char *s, glyphtable_t *glyph_table, size_t *outw, size_t *outh);
+int framebuffer_render_text(char *s, glyphtable_t *glyph_table, color_t *gbuf, size_t gbufwidth, size_t gbufheight, size_t offx);
+void console_render_buffer(color_t *data, size_t w, size_t h);
+
+#endif//__MAIN_H__
diff --git a/host/color.h b/host/color.h
index 1fbfe57..2a9d345 100644
--- a/host/color.h
+++ b/host/color.h
@@ -6,13 +6,14 @@
/* For easier memsetting we use an inverted alpha channel, i.e. 0 ≘ fully opaque; 255 ≘ fully transparent */
typedef struct {
- uint8_t r;
- uint8_t g;
- uint8_t b;
- uint8_t a;
+ uint8_t r, g, b, a;
} color_t;
typedef struct {
+ uint8_t r, g, b;
+} rgb_t;
+
+typedef struct {
color_t *data;
size_t w;
size_t h;
diff --git a/host/default.lines b/host/default.lines
new file mode 100644
index 0000000..050d78c
--- /dev/null
+++ b/host/default.lines
@@ -0,0 +1,88 @@
+\x1B[92mMate Light\x1B[93m@\x1B[92mPlay store or \x1B[94;101mtcp://ml.jaseg.net:1337\x1B[0;91m ♥
+\x1B[92mMate Light\x1B[0;91m ♥ \x1B[92mUnicode
+\x1B[92mMate Light\x1B[0;91m ♥ \x1B[92mANSI\x1B[0m \x1B[0;92;33;5mescape\x1B[0m \x1B[96msequences
+A customer enters a pet shop.
+\x1B[92m'Ello, I wish to register a complaint.
+(The owner does not respond.)
+\x1B[92m'Ello, Miss?
+\x1B[93mWhat do you mean "miss"?
+\x1B[92m(pause)I'm sorry, I have a cold. I wish to make a complaint!
+\x1B[93mWe're closin' for lunch.
+\x1B[92mNever mind that, my lad. I wish to complain about this parrot what I purchased not half an hour ago from this very boutique.
+\x1B[93mOh yes, the, uh, the Norwegian Blue...What's,uh...What's wrong with it?
+\x1B[92mI'll tell you what's wrong with it, my lad. 'E's dead, that's what's wrong with it!
+\x1B[93mNo, no, 'e's uh,...he's resting.
+\x1B[92mLook, matey, I know a dead parrot when I see one, and I'm looking at one right now.
+\x1B[93mNo no he's not dead, he's, he's restin'! Remarkable bird, the Norwegian Blue, idn'it, ay? Beautiful plumage!
+\x1B[92mThe plumage don't enter into it. It's stone dead.
+\x1B[93mNononono, no, no! 'E's resting!
+\x1B[92mAll right then, if he's restin', I'll wake him up! (shouting at the cage) 'Ello, Mister Polly Parrot! I've got a lovely fresh cuttle fish for you if you show...
+(owner hits the cage)
+\x1B[93mThere, he moved!
+\x1B[92mNo, he didn't, that was you hitting the cage!
+\x1B[93mI never!!
+\x1B[92mYes, you did!
+\x1B[93mI never, never did anything...
+\x1B[92m(yelling and hitting the cage repeatedly) 'ELLO POLLY!!!!! Testing! Testing! Testing! Testing! This is your nine o'clock alarm call!
+(Takes parrot out of the cage and thumps its head on the counter. Throws it up in the air and watches it plummet to the floor.)
+\x1B[92mNow that's what I call a dead parrot.
+\x1B[93mNo, no.....No, 'e's stunned!
+\x1B[92mSTUNNED?!?
+\x1B[93mYeah! You stunned him, just as he was wakin' up! Norwegian Blues stun easily, major.
+\x1B[92mUm...now look...now look, mate, I've definitely 'ad enough of this. That parrot is definitely deceased, and when I purchased it not 'alf an hour ago, you assured me that its total lack of movement was due to it bein' tired and shagged out following a prolonged squawk.
+\x1B[93mWell, he's...he's, ah...probably pining for the fjords.
+\x1B[92mPININ' for the FJORDS?!?!?!? What kind of talk is that?, look, why did he fall flat on his back the moment I got 'im home?
+\x1B[93mThe Norwegian Blue prefers keepin' on it's back! Remarkable bird, id'nit, squire? Lovely plumage!
+\x1B[92mLook, I took the liberty of examining that parrot when I got it home, and I discovered the only reason that it had been sitting on its perch in the first place was that it had been NAILED there.
+\x1B[93mWell, o'course it was nailed there! If I hadn't nailed that bird down, it would have nuzzled up to those bars, bent 'em apart with its beak, and VOOM! Feeweeweewee!
+\x1B[92m"VOOM"?!? Mate, this bird wouldn't "voom" if you put four million volts through it! 'E's bleedin' demised!
+\x1B[93mNo no! 'E's pining!
+\x1B[92m'E's not pinin'! 'E's passed on! This parrot is no more! He has ceased to be! 'E's expired and gone to meet 'is maker! 'E's a stiff! Bereft of life, 'e rests in peace! If you hadn't nailed 'im to the perch 'e'd be pushing up the daisies! 'Is metabolic processes are now 'istory! 'E's off the twig! 'E's kicked the bucket, 'e's shuffled off 'is mortal coil, run down the curtain and joined the bleedin' choir invisible!! THIS IS AN EX-PARROT!!
+\x1B[93mWell, I'd better replace it, then. (he takes a quick peek behind the counter) Sorry squire, I've had a look 'round the back of the shop, and uh, we're right out of parrots.
+\x1B[92mI see. I see, I get the picture.
+\x1B[93m(pause) I got a slug.
+\x1B[92mPray, does it talk?
+\x1B[93mNnnnot really.
+\x1B[92mWELL IT'S HARDLY A BLOODY REPLACEMENT, IS IT?!!???!!?
+\x1B[93mN-no, I guess not. (gets ashamed, looks at his feet)
+\x1B[92mWell.
+\x1B[93m(quietly) D'you.... d'you want to come back to my place?
+\x1B[92m(looks around) Yeah, all right, sure.
+Alternate ending:
+\x1B[92m(sweet as sugar) Pray, does it talk?
+\x1B[93mNnnnot really.
+\x1B[92mWELL IT'S HARDLY A BLOODY REPLACEMENT, IS IT?!!???!!?
+\x1B[93mLook, if you go to my brother's pet shop in Bolton, he'll replace the parrot for you.
+\x1B[92mBolton, eh? Very well.
+(The customer leaves.)
+(The customer enters the same pet shop. The owner is putting on a false moustache.)
+\x1B[92mThis is Bolton, is it?
+\x1B[93m(with a fake mustache) No, it's Ipswitch.
+\x1B[92m(looking at the camera) That's inter-city rail for you.
+(Mr. Praine goes to the train station. He addresses a man standing behind a desk marked "Complaints".)
+\x1B[92mI wish to complain, British-Railways Person.
+\x1B[93mI DON'T HAVE TO DO THIS JOB, YOU KNOW!!!
+\x1B[92mI beg your pardon...?
+\x1B[93mI'm a qualified brain surgeon! I only do this job because I like being my own boss!
+\x1B[92mExcuse me, this is irrelevant, isn't it?
+\x1B[93mYeah, well it's not easy to pad these python files out to 150 lines, you know.
+\x1B[92mWell, I wish to complain. I got on the Bolton train and found myself deposited here in Ipswitch.
+\x1B[93mNo, this is Bolton.
+\x1B[92m(to the camera) The pet shop man's brother was lying!!
+\x1B[93mCan't blame British Rail for that.
+\x1B[92mIn that case, I shall return to the pet shop!
+\x1B[92mI understand this IS Bolton.
+\x1B[93m(still with the fake mustache) Yes?
+\x1B[92mYou told me it was Ipswitch!
+\x1B[93m...It was a pun.
+\x1B[92m(pause) A PUN?!?
+\x1B[93mNo, no...not a pun...What's that thing that spells the same backwards as forwards?
+\x1B[92m(Long pause) A palindrome...?
+\x1B[93mYeah, that's it!
+\x1B[92mIt's not a palindrome! The palindrome of "Bolton" would be "Notlob"!! It don't work!!
+\x1B[93mWell, what do you want?
+\x1B[92mI'm not prepared to pursue my line of inquiry any longer as I think this is getting too silly!
+Sergeant-Major: Quite agree, quite agree, too silly, far too silly... (takes customer by the arm) Come on, you, you've got to go do another sketch now! Come on... (he walks off stage left, followed by the director and cameramen, leaving the owner alone on the set)
+\x1B[93m(to the audience) Well! I never wanted to do this in the first place. I wanted to be... a lumberjack!
+(he takes off his white lab coat to reveal a checkered shirt and suspenders under it)
+Floating down the mighty rivers of British Columbia! With my best girl by my side!...
diff --git a/host/font.c b/host/font.c
index ee9adb3..59bc2ef 100644
--- a/host/font.c
+++ b/host/font.c
@@ -6,7 +6,8 @@
#include <string.h>
#include <errno.h>
-void render_glyph(glyph_t *g, color_t *buf, unsigned int bufwidth, unsigned int offx, unsigned int offy, color_t fg, color_t bg){
+/* Clips at buffer bounds */
+void render_glyph(glyph_t *g, color_t *buf, unsigned int bufwidth, int offx, unsigned int offy, color_t fg, color_t bg){
unsigned int bitmap_row_width = g->width/8;
uint8_t *bitmap = ((uint8_t *)g) + sizeof(glyph_t);
for(unsigned int y=0; y < g->height; y++){
@@ -16,9 +17,13 @@ void render_glyph(glyph_t *g, color_t *buf, unsigned int bufwidth, unsigned int
data |= bitmap[y*bitmap_row_width+i];
}
color_t *p = buf + (offy+y)*bufwidth + offx;
- for(unsigned int x=0; x < g->width; x++){
- color_t c = (data&(1<<(g->width-1))) ? fg : bg;
- *p++ = c;
+ /* Take care to only render what's within the framebuffer's bounds */
+ for(int x=0; (x < g->width) && (offx+x < (int)bufwidth); x++){
+ if(offx + x >= 0){
+ color_t c = (data&(1<<(g->width-1))) ? fg : bg;
+ *p = c;
+ }
+ p++;
data <<= 1;
}
}
diff --git a/host/font.h b/host/font.h
index e9fbb35..6a6f302 100644
--- a/host/font.h
+++ b/host/font.h
@@ -29,7 +29,6 @@ glyphtable_t *read_bdf(FILE *f);
void free_glyphtable(glyphtable_t *glyph_table);
-// Requires buf to point to a buffer at least of size glyph->width*glyph->height.
-void render_glyph(glyph_t *glyph, color_t *buf, unsigned int bufwidth, unsigned int offx, unsigned int offy, color_t fg, color_t bg);
+void render_glyph(glyph_t *g, color_t *buf, unsigned int bufwidth, int offx, unsigned int offy, color_t fg, color_t bg);
#endif//__FONT_H__
diff --git a/host/main.h b/host/main.h
deleted file mode 100644
index 5ace45e..0000000
--- a/host/main.h
+++ /dev/null
@@ -1,10 +0,0 @@
-#ifndef __MAIN_H__
-#define __MAIN_H__
-
-#include "color.h"
-#include "font.h"
-
-framebuffer_t *framebuffer_render_text(char *s, glyphtable_t *glyph_table);
-void console_render_buffer(color_t *data, size_t w, size_t h);
-
-#endif//__MAIN_H__
diff --git a/host/matelight.py b/host/matelight.py
index 7c4f6c4..1bafe66 100644
--- a/host/matelight.py
+++ b/host/matelight.py
@@ -1,7 +1,8 @@
-import usb
import colorsys
-import numpy as np
from itertools import product
+from ctypes import c_size_t, c_uint8, c_void_p, c_float, CDLL, Structure, POINTER
+import numpy as np
+import time
CRATE_WIDTH = 5
CRATE_HEIGHT = 4
@@ -10,25 +11,34 @@ CRATES_Y = 4
DISPLAY_WIDTH = CRATES_X*CRATE_WIDTH
DISPLAY_HEIGHT = CRATES_Y*CRATE_HEIGHT
-FRAME_SIZE = CRATE_WIDTH*CRATE_HEIGHT*3
+CRATE_SIZE = CRATE_WIDTH*CRATE_HEIGHT*3
+FRAME_SIZE = DISPLAY_WIDTH*DISPLAY_HEIGHT
+
+# Gamma factor
+GAMMA = 2.5
+
+# Brightness of the LEDs in percent. 1.0 means 100%.
+BRIGHTNESS = 1.0
+
+ml = CDLL('./libml.so')
+ml.matelight_open.restype = c_void_p
-dev = usb.core.find(idVendor=0x1cbe, idProduct=0x0003)
+if ml.matelight_usb_init():
+ raise OSError('Cannot initialize USB library')
+matelights = ml.matelight_open()
+if matelights is None:
+ raise ImportError('Cannot open any Mate Light devices')
+dbuf = np.zeros(DISPLAY_WIDTH*DISPLAY_HEIGHT*4, dtype=np.uint8)
def sendframe(framedata):
""" Send a frame to the display
The argument contains a h * w array of 3-tuples of (r, g, b)-data or 4-tuples of (r, g, b, a)-data where the a
channel is ignored.
"""
- # Gamma correction
- framedata = (((framedata/255) ** 2.5) * 255).astype(np.uint8)
- for cx, cy in product(range(CRATES_X), range(CRATES_Y)):
- datar = framedata[cy*CRATE_HEIGHT:(cy+1)*CRATE_HEIGHT, cx*CRATE_WIDTH:(cx+1)*CRATE_WIDTH, :3]
- data = datar.flat
- if len(data) != FRAME_SIZE:
- raise ValueError('Invalid frame data. Expected {} bytes, got {}.'.format(FRAME_SIZE, len(data)))
- # Send framebuffer data
- dev.write(0x01, bytes([0, cx, cy])+bytes(data))
- # Send latch command
- dev.write(0x01, b'\x01')
+ # just use the first Mate Light available
+ rgba = len(framedata) == DISPLAY_WIDTH*DISPLAY_HEIGHT*4
+ global dbuf
+ np.copyto(dbuf[:640*(3+rgba)], np.frombuffer(framedata, dtype=np.uint8))
+ ml.matelight_send_frame(matelights, dbuf.ctypes.data_as(POINTER(c_uint8)), c_size_t(CRATES_X), c_size_t(CRATES_Y), c_float(BRIGHTNESS), rgba)
diff --git a/host/matelight.service b/host/matelight.service
new file mode 100644
index 0000000..7592791
--- /dev/null
+++ b/host/matelight.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=MateLight Server
+
+[Service]
+WorkingDirectory=/root/matelight/host
+ExecStart=/root/matelight/host/server.py
+Type=simple
+
+[Install]
+WantedBy=multi-user.target
diff --git a/host/net.h b/host/net.h
deleted file mode 100644
index d8ec949..0000000
--- a/host/net.h
+++ /dev/null
@@ -1,18 +0,0 @@
-#ifndef __NET_H__
-#define __NET_H__
-
-#include "config.h"
-
-typedef struct {
- uint8_t r, g, b;
-} rgb_t;
-
-typedef struct {
- uint32_t magic, seq;
- uint16_t width, height;
- rgb_t data[DISPLAY_WIDTH*DISPLAY_HEIGHT];
-} ml_packet_t;
-
-#define UDP_BUF_SIZE sizeof(ml_packet_t)
-
-#endif//__NET_H__
diff --git a/host/server.py b/host/server.py
index ab3f7b5..7efca5e 100755
--- a/host/server.py
+++ b/host/server.py
@@ -1,22 +1,22 @@
#!/usr/bin/env python
from socketserver import *
+import socket
+import struct
+import zlib
from time import time, strftime, sleep
-from collections import namedtuple
-from itertools import product
+from collections import namedtuple, deque
+import itertools
import threading
import random
+import os
-from ctypes import CDLL, POINTER, c_void_p, Structure, c_uint8, c_size_t, cast, addressof
-
-import numpy as np
-
-from matelight import sendframe, DISPLAY_WIDTH, DISPLAY_HEIGHT
+from ctypes import *
+from matelight import sendframe, DISPLAY_WIDTH, DISPLAY_HEIGHT, FRAME_SIZE
UDP_TIMEOUT = 3.0
-
class COLOR(Structure):
_fields_ = [('r', c_uint8), ('g', c_uint8), ('b', c_uint8), ('a', c_uint8)]
@@ -26,134 +26,159 @@ class FRAMEBUFFER(Structure):
bdf = CDLL('./libbdf.so')
bdf.read_bdf_file.restype = c_void_p
bdf.framebuffer_render_text.restype = POINTER(FRAMEBUFFER)
+bdf.framebuffer_render_text.argtypes= [c_char_p, c_void_p, c_void_p, c_size_t, c_size_t, c_size_t]
unifont = bdf.read_bdf_file('unifont.bdf')
-def render_text(text):
+def compute_text_bounds(text):
assert unifont
- fb = bdf.framebuffer_render_text(bytes(str(text), 'UTF-8'), unifont)
- fbd = fb.contents
- buf = np.ctypeslib.as_array(cast(fbd.data, POINTER(c_uint8)), shape=(fbd.h, fbd.w, 4))
- # Set data pointer to NULL before freeing framebuffer struct to prevent free_framebuffer from also freeing the data
- # buffer that is now used by numpy
- #bdf.console_render_buffer(fb)
- fbd.data = cast(c_void_p(), POINTER(COLOR))
- bdf.free_framebuffer(fb)
- return buf
+ textbytes = bytes(str(text), 'UTF-8')
+ textw, texth = c_size_t(0), c_size_t(0)
+ res = bdf.framebuffer_get_text_bounds(textbytes, unifont, byref(textw), byref(texth))
+ if res:
+ raise ValueError('Invalid text')
+ return textw.value, texth.value
+
+cbuf = create_string_buffer(FRAME_SIZE*sizeof(COLOR))
+cbuflock = threading.Lock()
+def render_text(text, offset):
+ global cbuf
+ cbuflock.acquire()
+ textbytes = bytes(str(text), 'UTF-8')
+ res = bdf.framebuffer_render_text(textbytes, unifont, cbuf, DISPLAY_WIDTH, DISPLAY_HEIGHT, offset)
+ if res:
+ raise ValueError('Invalid text')
+ cbuflock.release()
+ return cbuf
+
+printlock = threading.Lock()
def printframe(fb):
- h,w,_ = fb.shape
- print('\033[s\033[H', end='')
- bdf.console_render_buffer(fb.ctypes.data_as(POINTER(c_uint8)), w, h)
- print('\033[0m\033[u', end='')
-
-def scroll(text):
- """ Returns whether it could scroll all the text uninterrupted """
- fb = render_text(text);
- h,w,_ = fb.shape
- for i in range(-DISPLAY_WIDTH,w+1):
- if current_entry.entrytype == 'udp' or (current_entry in defaulttexts and textqueue):
- return False
- leftpad = np.zeros((DISPLAY_HEIGHT, max(-i, 0), 4), dtype=np.uint8)
- framedata = fb[:, max(0, i):min(i+DISPLAY_WIDTH, w)]
- rightpad = np.zeros((DISPLAY_HEIGHT, min(DISPLAY_WIDTH, max(0, i+DISPLAY_WIDTH-w)), 4), dtype=np.uint8)
- dice = np.concatenate((leftpad, framedata, rightpad), 1)
- sendframe(dice)
- #printframe(dice)
- fb = render_text(text);
- return True
-
-QueueEntry = namedtuple('QueueEntry', ['entrytype', 'remote', 'timestamp', 'text'])
-defaulttexts = [QueueEntry('text', '127.0.0.1', 0, t) for t in [
- '\x1B[92mMate Light\x1B[93m@\x1B[92mPlay store or \x1B[94;101mtcp://matelight.cbrp3.c-base.org:1337\x1B[0;91m ♥',
- '\x1B[92mMate Light\x1B[0;91m ♥ \x1B[92mUnicode',
- '\x1B[92mMate Light\x1B[0m powered by \x1B[95mMicrosoft™ \x1B[96mMarquee Manager® Professional OEM',
- '\x1B[92mMate Light\x1B[0m powered by \x1B[95mData Becker™ \x1B[96mLaufschriftstudio 2000® Platinum Edition',
- '\x1B[92mMate Light\x1B[0m powered by \x1B[95mApple™ \x1B[96miScroll®',
- '\x1B[92mMate Light\x1B[0m powered by \x1B[95mSiemens™ \x1B[96mLaufschrift® v.0.1.2b fuer Intel™ Pentium®',
- ]]
-current_entry = random.choice(defaulttexts)
-conns = {}
-textqueue = []
+ printlock.acquire()
+ print('\0337\033[H', end='')
+ print('Rendering frame @{}'.format(time()))
+ bdf.console_render_buffer(fb, DISPLAY_WIDTH, DISPLAY_HEIGHT)
+ #print('\033[0m\033[KCurrently rendering', current_entry.entrytype, 'from', current_entry.remote, ':', current_entry.text, '\0338', end='')
+ printlock.release()
def log(*args):
- print(strftime('[%m-%d %H:%M:%S]'), *args)
+ printlock.acquire()
+ print(strftime('\x1B[93m[%m-%d %H:%M:%S]\x1B[0m'), ' '.join(str(arg) for arg in args), '\x1B[0m')
+ printlock.release()
+
+class TextRenderer:
+ def __init__(self, text):
+ self.text = text
+ self.width, _ = compute_text_bounds(text)
+
+ def __iter__(self):
+ for i in range(-DISPLAY_WIDTH, self.width):
+ #print('Rendering text @ pos {}'.format(i))
+ yield render_text(self.text, i)
+
+class MateLightUDPServer:
+ def __init__(self, port=1337, ip=''):
+ self.current_client = None
+ self.last_timestamp = 0
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ self.socket.bind((ip, port))
+ self.thread = threading.Thread(target = self.udp_receive)
+ self.thread.daemon = True
+ self.start = self.thread.start
+ self.frame_condition = threading.Condition()
+ self.frame = None
+
+ def frame_da(self):
+ return self.frame is not None
+
+ def __iter__(self):
+ while True:
+ with self.frame_condition:
+ if not self.frame_condition.wait_for(self.frame_da, timeout=UDP_TIMEOUT):
+ raise StopIteration()
+ frame, self.frame = self.frame, None
+ yield frame
+
+ def udp_receive(self):
+ while True:
+ try:
+ data, (addr, sport) = self.socket.recvfrom(FRAME_SIZE*3+4)
+ timestamp = time()
+ if timestamp - self.last_timestamp > UDP_TIMEOUT:
+ self.current_client = addr
+ log('\x1B[91mAccepting UDP data from\x1B[0m', addr)
+ if addr == self.current_client:
+ if len(data) == FRAME_SIZE*3+4:
+ frame = data[:-4]
+ crc1, = struct.unpack('!I', data[-4:])
+ if crc1:
+ crc2, = zlib.crc32(frame, 0),
+ if crc1 != crc2:
+ raise ValueError('Invalid frame CRC checksum: Expected {}, got {}'.format(crc2, crc1))
+ elif len(data) == FRAME_SIZE*3:
+ frame = data
+ else:
+ raise ValueError('Invalid frame size: {}'.format(len(data)))
+ self.last_timestamp = timestamp
+ with self.frame_condition:
+ self.frame = frame
+ self.frame_condition.notify()
+ except Exception as e:
+ log('Error receiving UDP frame:', e)
-class MateLightUDPHandler(BaseRequestHandler):
- def handle(self):
- try:
- global current_entry, conns
- data = self.request[0].strip()
- if len(data) != FRAME_SIZE+4:
- #raise ValueError('Invalid frame size: Expected {}, got {}'.format(FRAME_SIZE+4, len(data)))
- return
- frame = data[:-4]
- #crc1, = struct.unpack('!I', data[-4:])
- #crc2, = zlib.crc32(frame, 0),
- #if crc1 != crc2:
- # raise ValueError('Invalid frame CRC checksum: Expected {}, got {}'.format(crc2, crc1))
- #socket.sendto(b'ACK', self.client_address)
- a = np.array(list(frame), dtype=np.uint8)
- timestamp = time()
- addr = self.client_address[0]
- if addr not in conns:
- current_entry = QueueEntry('udp', addr, timestamp, '')
- conns[addr] = current_entry
- log('New UDP connection from', addr)
- else:
- conns[addr].timestamp = timestamp
- if current_entry.entrytype == 'udp' and current_entry.remote == addr:
- frame = a.reshape((DISPLAY_HEIGHT, DISPLAY_WIDTH, 3))
- sendframe(frame)
- #printframe(frame)
- except Exception as e:
- print('Error receiving UDP frame:', e)
- ex_type, ex, tb = sys.exc_info()
- traceback.print_tb(tb)
+renderqueue = deque()
class MateLightTCPTextHandler(BaseRequestHandler):
def handle(self):
- global current_entry, conns
+ global render_deque
data = str(self.request.recv(1024).strip(), 'UTF-8')
addr = self.client_address[0]
- timestamp = time()
if len(data) > 140:
- self.request.sendall('TOO MUCH INFORMATION!\n')
+ self.request.sendall(b'TOO MUCH INFORMATION!\n')
return
- log('Text from {}: {}\x1B[0m'.format(addr, data))
- textqueue.append(QueueEntry('text', addr, timestamp, data))
+ log('\x1B[95mText from\x1B[0m {}: {}\x1B[0m'.format(addr, data))
+ renderqueue.append(TextRenderer(data))
self.request.sendall(b'KTHXBYE!\n')
TCPServer.allow_reuse_address = True
-server = TCPServer(('', 1337), MateLightTCPTextHandler)
-t = threading.Thread(target=server.serve_forever)
+tserver = TCPServer(('', 1337), MateLightTCPTextHandler)
+t = threading.Thread(target=tserver.serve_forever)
t.daemon = True
t.start()
-UDPServer.allow_reuse_address = True
-userver = UDPServer(('', 1337), MateLightUDPHandler)
-t = threading.Thread(target=userver.serve_forever)
-t.daemon = True
-t.start()
+userver = MateLightUDPServer()
+userver.start()
+
+defaultlines = [ TextRenderer(l[:-1].replace('\\x1B', '\x1B')) for l in open('default.lines').readlines() ]
+#random.shuffle(defaultlines)
+defaulttexts = itertools.chain(*defaultlines)
if __name__ == '__main__':
- print('\n'*7)
+ print('\033[?1049h'+'\n'*9)
while True:
- if current_entry.entrytype == 'text':
- if scroll(current_entry.text):
- textqueue.remove(current_entry)
- if textqueue:
- current_entry = textqueue[0]
- else:
- if conns:
- current_entry = random.choice(conns.values())
- else:
- current_entry = random.choice(defaulttexts)
- if current_entry.entrytype != 'udp' and textqueue:
- current_entry = textqueue[0]
- if current_entry.entrytype == 'udp':
- if time() - current_entry.timestamp > UDP_TIMEOUT:
- current_entry = random.choice(defaulttexts)
+ if renderqueue:
+ renderer = renderqueue.popleft()
+ elif userver.frame_da():
+ renderer = userver
+ else:
+ static_noise = False #time() % 300 < 60
+ if static_noise:
+ foo = os.urandom(640)
+ frame = bytes([v for c in zip(list(foo), list(foo), list(foo)) for v in c ])
else:
- sleep(0.2)
+ try:
+ frame = next(defaulttexts)
+ except StopIteration:
+ defaultlines = [ TextRenderer(l[:-1].replace('\\x1B', '\x1B')) for l in open('default.lines').readlines() ]
+ #random.shuffle(defaultlines)
+ defaulttexts = itertools.chain(*defaultlines)
+ sendframe(frame)
+# printframe(frame)
+ continue
+# sleep(0.1)
+ for frame in renderer:
+ sendframe(frame)
+# printframe(frame)
+# sleep(0.1)
diff --git a/host/terminal.py b/host/terminal.py
new file mode 100644
index 0000000..86b4db1
--- /dev/null
+++ b/host/terminal.py
@@ -0,0 +1,19 @@
+from pixelterm.pixelterm import termify_pixels
+
+class MockImage:
+ def __init__(self, nparray):
+ self.nparray = nparray
+ self.im = self
+
+ @property
+ def size(self):
+ h, w, _ = self.nparray.shape
+ return (w, h)
+
+ def getpixel(self, pos):
+ x, y = pos
+ return tuple(self.nparray[y][x])
+
+def printframe(framedata):
+ print(termify_pixels(MockImage(framedata)))
+
diff --git a/host/usb.c b/host/usb.c
new file mode 100644
index 0000000..5e696d5
--- /dev/null
+++ b/host/usb.c
@@ -0,0 +1,161 @@
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <libusb-1.0/libusb.h>
+
+#include "color.h"
+#include "usb.h"
+
+int matelight_usb_init(){
+ int rc = 0;
+ rc = libusb_init(NULL);
+ if(rc){
+ fprintf(stderr, "LIBML: Cannot initialize libusb\n");
+ return 1;
+ }
+ return 0;
+}
+
+void matelight_usb_destroy(){
+ libusb_exit(NULL);
+}
+
+static int matelight_cmp_str_desc(libusb_device_handle *dev, uint8_t index, char *value){
+ char data[256];
+ if(libusb_get_string_descriptor_ascii(dev, index, (unsigned char*)data, sizeof(data)) < 0){
+ fprintf(stderr, "LIBML: Cannot read device string descriptor\n");
+ return 0;
+ }
+ return strcmp(data, value) == 0;
+}
+
+matelight_handle *matelight_open(){
+ matelight_handle *out = NULL;
+ libusb_device** device_list;
+ struct libusb_device_descriptor desc;
+ ssize_t res = libusb_get_device_list(NULL, &device_list);
+ if(res == 0){
+ fprintf(stderr, "LIBML: Cannot find any connected matelight\n");
+ goto error;
+ }else if(res < 0){
+ fprintf(stderr, "LIBML: Error enumerating connected USB devices\n");
+ goto error;
+ }else{
+ out = calloc(res+1, sizeof(matelight_handle));
+ if(!out){
+ fprintf(stderr, "LIBML: Cannot allocate memory\n");
+ goto error;
+ }
+ memset(out, 0, (res+1)*sizeof(matelight_handle));
+ unsigned int found = 0;
+ for(ssize_t i=0; i<res; i++){
+ libusb_get_device_descriptor(device_list[i], &desc);
+ if(desc.idVendor == MATELIGHT_VID && desc.idProduct == MATELIGHT_PID){
+ libusb_device_handle *handle;
+ if(libusb_open(device_list[i], &(handle))){
+ fprintf(stderr, "LIBML: Cannot open Mate Light USB device\n");
+ goto error;
+ }
+ out[found].handle = handle;
+ if(matelight_cmp_str_desc(handle, desc.iManufacturer, "Gold & Apple"))
+ if(matelight_cmp_str_desc(handle, desc.iProduct, "Mate Light")){
+#define BUF_SIZE 256
+ char *serial = malloc(BUF_SIZE);
+ if(!serial){
+ fprintf(stderr, "LIBML: Cannot allocate memory\n");
+ goto error;
+ }
+ if(libusb_get_string_descriptor_ascii(out[found].handle, desc.iSerialNumber, (unsigned char*)serial, BUF_SIZE) < 0){
+ fprintf(stderr, "LIBML: Cannot read device string descriptor\n");
+ goto error;
+ }
+#undef BUF_SIZE
+ out[found].serial = serial;
+ found++;
+ }
+ }
+ }
+ out[found].handle = NULL;
+ out[found].serial = NULL;
+ }
+ libusb_free_device_list(device_list, 1);
+ return out;
+error:
+ if(res>0 && out){
+ for(ssize_t i=0; i<res; i++){
+ if(out[i].handle)
+ libusb_close(out[i].handle);
+ free(out[i].serial);
+ }
+ }
+ free(out);
+ libusb_free_device_list(device_list, 1);
+ return 0;
+}
+
+typedef struct{
+ uint8_t opcode;
+ uint8_t x, y;
+ rgb_t buf[CRATE_WIDTH*CRATE_HEIGHT];
+} crate_frame_t;
+
+int matelight_send_frame(matelight_handle *ml, void *buf, size_t w, size_t h, float brightness, int alpha){
+// fprintf(stderr, "\nFRAME START\n");
+ for(size_t cy=0; cy<h; cy++){
+// fprintf(stderr, "##### NEXT ROW\n");
+ for(size_t cx=0; cx<w; cx++){
+ crate_frame_t frame;
+ frame.opcode = 0;
+ frame.x = cx;
+ frame.y = cy;
+// fprintf(stderr, "CRATE %d %d\n", cx, cy);
+ for(size_t x=0; x<CRATE_WIDTH; x++){
+ for(size_t y=0; y<CRATE_HEIGHT; y++){
+ size_t dpos = y*CRATE_WIDTH + x;
+ size_t spos = w*CRATE_WIDTH*(cy*CRATE_HEIGHT+y) + cx*CRATE_WIDTH+x;
+ color_t *src;
+ if(alpha){
+ src = (((color_t*)buf)+spos);
+ }else{
+ src = (color_t*)(((rgb_t*)buf)+spos);
+ }
+ rgb_t *dst = frame.buf+dpos;
+ /* Gamma correction */
+#define GAMMA_APPLY(c) ((uint8_t)roundf(powf((c/255.0F), GAMMA) * brightness * 255))
+ dst->r = GAMMA_APPLY(src->r);
+ dst->g = GAMMA_APPLY(src->g);
+ dst->b = GAMMA_APPLY(src->b);
+// fprintf(stderr, "%c", ((dst->r > 1) ? '#' : '.'));
+#undef GAMMA_APPLY
+ }
+// fprintf(stderr, "\n");
+ }
+ int transferred = 0;
+ int rc = libusb_bulk_transfer(ml->handle, MATELIGHT_FRAMEDATA_ENDPOINT, (unsigned char*)&frame, sizeof(frame), &transferred, MATELIGHT_TIMEOUT);
+ if(rc){
+ fprintf(stderr, "LIBML: Cannot write frame data\n");
+ return 1;
+ }
+ if(transferred != sizeof(frame)){
+ fprintf(stderr, "LIBML: Could not transfer all frame data. Only transferred %d out of %d bytes.\n", transferred, sizeof(frame));
+ return 1;
+ }
+ }
+ }
+ uint8_t payload = 0x01;
+ int transferred = 0;
+ int rc = libusb_bulk_transfer(ml->handle, MATELIGHT_FRAMEDATA_ENDPOINT, &payload, sizeof(uint8_t), &transferred, MATELIGHT_TIMEOUT);
+ if(rc){
+ fprintf(stderr, "LIBML: Cannot write frame data\n");
+ return 1;
+ }
+ if(transferred != sizeof(uint8_t)){
+ fprintf(stderr, "LIBML: Could not transfer all frame data. Only transferred %d out of %d bytes.\n", transferred, sizeof(uint8_t));
+ return 1;
+ }
+ return 0;
+}
+
diff --git a/host/usb.h b/host/usb.h
new file mode 100644
index 0000000..fee3487
--- /dev/null
+++ b/host/usb.h
@@ -0,0 +1,27 @@
+#ifndef __USB_H__
+#define __USB_H__
+
+#include <libusb-1.0/libusb.h>
+
+#define MATELIGHT_VID 0x1cbe
+#define MATELIGHT_PID 0x0003
+
+#define CRATE_WIDTH 5
+#define CRATE_HEIGHT 4
+
+#define MATELIGHT_FRAMEDATA_ENDPOINT 0x01
+#define MATELIGHT_TIMEOUT 1000
+
+#define GAMMA 2.5F
+
+typedef struct {
+ libusb_device_handle *handle;
+ char *serial;
+} matelight_handle;
+
+int matelight_usb_init(void);
+void matelight_usb_destroy(void);
+matelight_handle *matelight_open(void);
+int matelight_send_frame(matelight_handle *ml, void *buf, size_t w, size_t h, float brightness, int alpha);
+
+#endif//__USB_H__