Compare commits
No commits in common. "main" and "master" have entirely different histories.
33 changed files with 1471 additions and 171 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
*.csv
|
||||
*.pdftables
|
||||
*.pdf
|
||||
__pycache__
|
||||
secrets.py
|
||||
.vscode
|
15
Makefile
Normal file
15
Makefile
Normal file
|
@ -0,0 +1,15 @@
|
|||
MAKEFILES = $(wildcard */Makefile)
|
||||
SUBDIRS = $(MAKEFILES:%/Makefile=%)
|
||||
|
||||
all: build check
|
||||
|
||||
build: $(SUBDIRS:%=build-%)
|
||||
|
||||
check: $(SUBDIRS:%=check-%)
|
||||
|
||||
build-%:
|
||||
$(MAKE) -C $*
|
||||
|
||||
check-%:
|
||||
! grep ^test: $*/Makefile || $(MAKE) -C $* test
|
||||
! grep ^check: $*/Makefile || $(MAKE) -C $* check
|
|
@ -1,5 +0,0 @@
|
|||
# mqtt_rewrite
|
||||
|
||||
Make SolaX inverters work with Mosquitto by fixing a bug in the SolaX MQTT implementation.
|
||||
|
||||
See https://juju.nz/michaelh/post/2021/solax/ for details.
|
151
charger/charger.scad
Normal file
151
charger/charger.scad
Normal file
|
@ -0,0 +1,151 @@
|
|||
include <threadlib/threadlib.scad>;
|
||||
|
||||
$fn = 100;
|
||||
|
||||
// https://www.aliexpress.com/item/1005005144919264.html?spm=a2g0o.order_list.order_list_main.5.d8bc1802v8boh9
|
||||
charger_r1 = 37 / 2; // Top radius.
|
||||
charger_r2 = 28.8 / 2; // Body outer thread radius.
|
||||
charger_thread = 1; // Thread depth.
|
||||
charger_r3 = charger_r2 - charger_thread;
|
||||
charger_r4 = 37/2; // Nut radius.
|
||||
charger_pitch = 13.9/9; // Thread ptich.
|
||||
charger_h1 = 3; // Cap height.
|
||||
charger_h2 = 36.3; // Body height minus cap.
|
||||
lugs_s = [6.3, 11, 10];
|
||||
charger_s = [charger_r1, charger_r1, charger_h1 + charger_h2];
|
||||
|
||||
// https://www.gobilda.com/xt60-connector-pack-mh-fc-x-5-fh-mc-x-5/
|
||||
xt60_s = [ 15.8, 8.1, 16 ];
|
||||
xt60_wall = 0.6; // Inner wall thickness.
|
||||
xt60_h1 = 10.7; // Inner cutout depth.
|
||||
xt60_i = 2.6;
|
||||
|
||||
wall = 1.3;
|
||||
tol = 0.25;
|
||||
e = 0.004;
|
||||
e_z = [ 0, 0, e ];
|
||||
inf = 100;
|
||||
inf_z = [ 0, 0, inf ];
|
||||
|
||||
module xt60_base(x, y, h) {
|
||||
i = xt60_i * x / xt60_s.x;
|
||||
translate([-x/2, -y/2, 0])
|
||||
linear_extrude(h)
|
||||
polygon([[0, 0],
|
||||
[x-i, 0],
|
||||
[x, i],
|
||||
[x, y-i],
|
||||
[x-i, y],
|
||||
[0, y]]);
|
||||
}
|
||||
|
||||
module xt60() {
|
||||
w = xt60_wall;
|
||||
difference() {
|
||||
xt60_base(xt60_s.x, xt60_s.y, xt60_s.z);
|
||||
translate([w,w,-e])
|
||||
xt60_base(xt60_s.x-w*2, xt60_s.y-w*2, xt60_h1);
|
||||
}
|
||||
}
|
||||
|
||||
module cap(r, h, inset) {
|
||||
// cylinder(r=charger_r1, h=charger_h1);
|
||||
rotate_extrude() {
|
||||
polygon([
|
||||
[0, 0],
|
||||
[r, 0],
|
||||
[r, h-inset],
|
||||
[r-inset, h],
|
||||
[0, h],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
module charger() {
|
||||
translate([0, 0, lugs_s.z]) {
|
||||
translate([0, 0, charger_h2])
|
||||
cap(charger_r1, charger_h1, 1.5);
|
||||
cylinder(r=charger_r3, h=charger_h2);
|
||||
translate([0, 0, 5])
|
||||
cylinder(r=charger_r2, h=charger_h2-5);
|
||||
}
|
||||
translate([-lugs_s.x/2, -lugs_s.y/2, 0]) {
|
||||
cube([lugs_s.x, 0.3, lugs_s.z]);
|
||||
translate([0, lugs_s.y, 0])
|
||||
cube([lugs_s.x, 0.3, lugs_s.z]);
|
||||
}
|
||||
}
|
||||
|
||||
module upcap(r, h, inset) {
|
||||
translate([0, 0, h])
|
||||
mirror([0, 0, 1])
|
||||
cap(r,h,inset);
|
||||
}
|
||||
|
||||
module body() {
|
||||
w = charger_r4;
|
||||
difference() {
|
||||
upcap(w+wall, charger_h2+lugs_s.z+10, 1.5);
|
||||
translate([0, 0, wall])
|
||||
upcap(w+wall/2, charger_h2*2, 1.5);
|
||||
xt60_hole();
|
||||
}
|
||||
difference() {
|
||||
xt60_sheath();
|
||||
translate(-e_z)
|
||||
xt60_hole();
|
||||
}
|
||||
}
|
||||
|
||||
module top_cap() {
|
||||
h = 15;
|
||||
w = charger_r4+wall+tol/2;
|
||||
difference() {
|
||||
union() {
|
||||
translate([0, 0, h/2])
|
||||
cap(w+wall, h/2, 1.5);
|
||||
upcap(w+wall, h/2, 1);
|
||||
}
|
||||
translate([0, 0, -wall])
|
||||
cap(w, h, 1.5);
|
||||
cylinder(r=charger_r2+tol, h=inf);
|
||||
cut = 20;
|
||||
translate([(charger_r2+charger_r1+wall)/2+cut/2, 0, 0])
|
||||
cylinder(r=cut/2, h=30);
|
||||
translate([-(charger_r2+charger_r1+wall)/2-cut/2, 0, 0])
|
||||
cylinder(r=cut/2, h=30);
|
||||
}
|
||||
}
|
||||
|
||||
module xt60_sheath() {
|
||||
xt60_base(xt60_s.x+wall*2, xt60_s.y+wall*2, xt60_s.z/2);
|
||||
}
|
||||
|
||||
module xt60_hole() {
|
||||
xt60_base(xt60_s.x+tol*2, xt60_s.y+tol*2, 20);
|
||||
}
|
||||
|
||||
module test() {
|
||||
translate([charger_r1*3, 0, xt60_s.z])
|
||||
charger();
|
||||
|
||||
translate([0, charger_r1*3, 0])
|
||||
xt60();
|
||||
|
||||
translate([0, 0, 60])
|
||||
top_cap();
|
||||
|
||||
body();
|
||||
}
|
||||
|
||||
module print() {
|
||||
body();
|
||||
// translate([0, 50, 10])
|
||||
// mirror([0,0,1]) top_cap();
|
||||
}
|
||||
|
||||
//top_cap();
|
||||
//test();
|
||||
print();
|
||||
//body();
|
||||
//# cube([1, 41.6-wall*2, 1], true);
|
35
christmas/Makefile
Normal file
35
christmas/Makefile
Normal file
|
@ -0,0 +1,35 @@
|
|||
APP = christmas
|
||||
DEVICE = attiny85
|
||||
CLOCK = 16500000
|
||||
PROGRAMMER = -c signalyzer-lite
|
||||
SRC = $(wildcard *.cc)
|
||||
OBJ = $(SRC:%.cc=%.o)
|
||||
FUSES = -U lfuse:w:0xe2:m -U hfuse:w:0xde:m -U efuse:w:0xfe:m
|
||||
|
||||
CROSS_COMPILE = avr-
|
||||
CC = $(CROSS_COMPILE)gcc
|
||||
CXX = $(CROSS_COMPILE)g++
|
||||
CFLAGS = -Os -DF_CPU=$(CLOCK) -mmcu=$(DEVICE) -Wall
|
||||
CXXFLAGS = $(CFLAGS) -std=gnu++11
|
||||
AVRDUDE = avrdude $(PROGRAMMER) -p $(DEVICE)
|
||||
|
||||
all: $(APP).hex
|
||||
|
||||
flash: all
|
||||
$(AVRDUDE) -U flash:w:$(APP).hex:i
|
||||
|
||||
usb-flash: $(APP).hex
|
||||
sudo $(HOME)/bin/micronucleus --run $<
|
||||
|
||||
fuse:
|
||||
$(AVRDUDE) $(FUSES)
|
||||
|
||||
%.elf: $(OBJ)
|
||||
$(CXX) $(CXXFLAGS) -o $@ $(OBJ)
|
||||
$(CROSS_COMPILE)size -d $@
|
||||
|
||||
%.hex: %.elf
|
||||
avr-objcopy -j .text -j .data -O ihex $< $@
|
||||
|
||||
clean:
|
||||
rm -f $(APP).hex $(APP).elf $(OBJ) *~
|
188
christmas/christmas.cc
Normal file
188
christmas/christmas.cc
Normal file
|
@ -0,0 +1,188 @@
|
|||
/// Christmas star lights.
|
||||
///
|
||||
/// Mainly white, but transitions white -> colour -> white and cycles
|
||||
/// through colours.
|
||||
///
|
||||
/// Michael Hope <michaelh@juju.net.nz> 2013
|
||||
///
|
||||
|
||||
#include <avr/io.h>
|
||||
#include <avr/interrupt.h>
|
||||
#include <avr/sleep.h>
|
||||
|
||||
class HAL
|
||||
{
|
||||
public:
|
||||
enum Clock {
|
||||
Timer0Prescaler = 64,
|
||||
Timer0Rate = F_CPU / Timer0Prescaler / 256,
|
||||
};
|
||||
|
||||
enum class Pin {
|
||||
};
|
||||
};
|
||||
|
||||
class Christmas
|
||||
{
|
||||
public:
|
||||
static void init();
|
||||
static void run();
|
||||
|
||||
static volatile uint8_t ticks;
|
||||
|
||||
private:
|
||||
/// A single point in the cycle.
|
||||
struct Point
|
||||
{
|
||||
/// How long to stay at this point.
|
||||
uint8_t seconds;
|
||||
uint8_t r, g, b;
|
||||
};
|
||||
|
||||
static void delay(uint16_t count);
|
||||
static uint8_t correct(uint8_t v);
|
||||
static uint8_t project(uint8_t from, uint8_t to, uint8_t at, uint8_t limit);
|
||||
|
||||
static const uint8_t ciel8_[];
|
||||
static const Point points_[];
|
||||
};
|
||||
|
||||
volatile uint8_t Christmas::ticks;
|
||||
|
||||
// Output compare comes out of:
|
||||
// OC0A (PB0)
|
||||
// OC0B (PB1)
|
||||
// OC1B (PB4)
|
||||
|
||||
void Christmas::init()
|
||||
{
|
||||
DDRB = _BV(0) | _BV(1) | _BV(4);
|
||||
|
||||
// Clear the output on compare.
|
||||
TCCR0A = 0
|
||||
| (3 << COM0A0)
|
||||
| (3 << COM0B0)
|
||||
// Fast PWM.
|
||||
| (3 << WGM00)
|
||||
;
|
||||
|
||||
static_assert(HAL::Timer0Prescaler == 64, "Update the prescaler below.");
|
||||
TCCR0B = 0
|
||||
// Prescale by 64.
|
||||
| (3 << CS00)
|
||||
;
|
||||
|
||||
TIMSK = _BV(TOIE0);
|
||||
|
||||
TCCR1 = 0
|
||||
| (3 << CS10)
|
||||
;
|
||||
|
||||
GTCCR = 0
|
||||
| (1 << PWM1B)
|
||||
| (2 << COM1B0)
|
||||
;
|
||||
}
|
||||
|
||||
/// Convert an intensity to PWM.
|
||||
const uint8_t Christmas::ciel8_[] = {
|
||||
0, 0, 0, 0, 0, 1, 1, 1,
|
||||
1, 1, 1, 2, 2, 2, 2, 3,
|
||||
3, 3, 4, 4, 5, 5, 6, 7,
|
||||
7, 8, 9, 10, 11, 12, 13, 14,
|
||||
16, 17, 19, 21, 23, 25, 27, 30,
|
||||
33, 36, 39, 43, 47, 51, 56, 61,
|
||||
67, 73, 80, 88, 96, 105, 115, 125,
|
||||
137, 149, 163, 178, 195, 213, 233, 255,
|
||||
};
|
||||
|
||||
#define MAX 63
|
||||
|
||||
/// All points to cycle through.
|
||||
const Christmas::Point Christmas::points_[] =
|
||||
{
|
||||
{ 60, MAX, MAX, MAX }, // White
|
||||
{ 10, 0, 0, MAX }, // Blue
|
||||
{ 60, MAX, MAX, MAX },
|
||||
{ 10, MAX, 0, MAX }, // Purple
|
||||
{ 60, MAX, MAX, MAX },
|
||||
{ 10, MAX, 0, 0 }, // Red
|
||||
{ 60, MAX, MAX, MAX },
|
||||
{ 10, MAX, MAX, 0 }, // Yellow
|
||||
{ 60, MAX, MAX, MAX },
|
||||
{ 10, 0, MAX, 0 }, // Green
|
||||
{ 60, MAX, MAX, MAX },
|
||||
{ 10, 0, MAX, MAX }, // Cyan
|
||||
// End of list.
|
||||
{ 0 },
|
||||
};
|
||||
|
||||
/// Convert intensity to PWM.
|
||||
uint8_t Christmas::correct(uint8_t level)
|
||||
{
|
||||
return ciel8_[level];
|
||||
}
|
||||
|
||||
/// Mix the from and to levels.
|
||||
uint8_t Christmas::project(uint8_t from, uint8_t to, uint8_t at, uint8_t limit)
|
||||
{
|
||||
int delta = to - from;
|
||||
delta = delta * at / limit;
|
||||
|
||||
return (uint8_t)(from + delta);
|
||||
}
|
||||
|
||||
/// Delay the given number of ticks.
|
||||
void Christmas::delay(uint16_t count)
|
||||
{
|
||||
uint8_t now = ticks;
|
||||
|
||||
while (count != 0) {
|
||||
while (now == ticks) {
|
||||
sleep_mode();
|
||||
}
|
||||
|
||||
count--;
|
||||
now++;
|
||||
}
|
||||
}
|
||||
|
||||
void Christmas::run()
|
||||
{
|
||||
sei();
|
||||
|
||||
const Point* plast = points_ + 0;
|
||||
|
||||
for (;;) {
|
||||
for (const Point* ppoint = points_; ppoint->seconds != 0; ppoint++) {
|
||||
const int steps = 64;
|
||||
|
||||
// Transition between colours.
|
||||
for (int i = 0; i < steps; i++) {
|
||||
OCR0A = correct(project(plast->r, ppoint->r, i, steps));
|
||||
OCR0B = correct(project(plast->g, ppoint->g, i, steps));
|
||||
OCR1B = 255 - correct(project(plast->b, ppoint->b, i, steps));
|
||||
|
||||
// Change over 10s.
|
||||
delay(HAL::Clock::Timer0Rate * 10 / steps);
|
||||
}
|
||||
plast = ppoint;
|
||||
|
||||
// Delay on this colour.
|
||||
delay(HAL::Clock::Timer0Rate * ppoint->seconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ISR(TIMER0_OVF_vect)
|
||||
{
|
||||
Christmas::ticks++;
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
Christmas::init();
|
||||
Christmas::run();
|
||||
|
||||
return 0;
|
||||
}
|
35
chroma/Makefile
Normal file
35
chroma/Makefile
Normal file
|
@ -0,0 +1,35 @@
|
|||
APP = chroma
|
||||
DEVICE = attiny85
|
||||
CLOCK = 16500000
|
||||
PROGRAMMER = -c signalyzer-lite
|
||||
SRC = $(wildcard *.cc)
|
||||
OBJ = $(SRC:%.cc=%.o)
|
||||
FUSES = -U lfuse:w:0xe2:m -U hfuse:w:0xde:m -U efuse:w:0xfe:m
|
||||
|
||||
CROSS_COMPILE = avr-
|
||||
CC = $(CROSS_COMPILE)gcc
|
||||
CXX = $(CROSS_COMPILE)g++
|
||||
CFLAGS = -Os -DF_CPU=$(CLOCK) -mmcu=$(DEVICE) -Wall
|
||||
CXXFLAGS = $(CFLAGS) -std=gnu++11
|
||||
AVRDUDE = avrdude $(PROGRAMMER) -p $(DEVICE)
|
||||
|
||||
all: $(APP).hex
|
||||
|
||||
flash: all
|
||||
$(AVRDUDE) -U flash:w:$(APP).hex:i
|
||||
|
||||
usb-flash: $(APP).hex
|
||||
sudo $(HOME)/bin/micronucleus --run $<
|
||||
|
||||
fuse:
|
||||
$(AVRDUDE) $(FUSES)
|
||||
|
||||
%.elf: $(OBJ)
|
||||
$(CXX) $(CXXFLAGS) -o $@ $(OBJ)
|
||||
$(CROSS_COMPILE)size -d $@
|
||||
|
||||
%.hex: %.elf
|
||||
avr-objcopy -j .text -j .data -O ihex $< $@
|
||||
|
||||
clean:
|
||||
rm -f $(APP).hex $(APP).elf $(OBJ) *~
|
189
chroma/chroma.cc
Normal file
189
chroma/chroma.cc
Normal file
|
@ -0,0 +1,189 @@
|
|||
/// Flash an RGB LED through all colours.
|
||||
#include <avr/io.h>
|
||||
#include <avr/interrupt.h>
|
||||
#include <avr/sleep.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
class HAL
|
||||
{
|
||||
public:
|
||||
enum Clock {
|
||||
Timer0Prescaler = 64,
|
||||
Timer0Rate = F_CPU / Timer0Prescaler / 256,
|
||||
};
|
||||
|
||||
enum class Pin {
|
||||
};
|
||||
};
|
||||
|
||||
class Chroma
|
||||
{
|
||||
public:
|
||||
static void init();
|
||||
static void run();
|
||||
|
||||
static volatile uint8_t ticks;
|
||||
|
||||
private:
|
||||
struct RGB
|
||||
{
|
||||
int16_t r, g, b;
|
||||
};
|
||||
|
||||
static void delay(uint8_t count);
|
||||
static uint8_t clip(int16_t v);
|
||||
static uint8_t correct(int16_t v);
|
||||
static void hsv_to_rgb(int16_t hdash, RGB* rgb);
|
||||
|
||||
static const int16_t Scale = 256/2;
|
||||
static const int16_t One = Scale - 1;
|
||||
|
||||
static const uint8_t ciel8[];
|
||||
};
|
||||
|
||||
volatile uint8_t Chroma::ticks;
|
||||
|
||||
// Output compare comes out of:
|
||||
// OC0A (PB0)
|
||||
// OC0B (PB1)
|
||||
// OC1B (PB4)
|
||||
|
||||
void Chroma::init()
|
||||
{
|
||||
DDRB = _BV(0) | _BV(1) | _BV(4);
|
||||
|
||||
// Clear the output on compare.
|
||||
TCCR0A = 0
|
||||
| (3 << COM0A0)
|
||||
| (3 << COM0B0)
|
||||
// Fast PWM.
|
||||
| (3 << WGM00)
|
||||
;
|
||||
|
||||
static_assert(HAL::Timer0Prescaler == 64, "Update the prescaler below.");
|
||||
TCCR0B = 0
|
||||
// Prescale by 64.
|
||||
| (3 << CS00)
|
||||
;
|
||||
|
||||
TIMSK = _BV(TOIE0);
|
||||
|
||||
TCCR1 = 0
|
||||
| (3 << CS10)
|
||||
;
|
||||
|
||||
GTCCR = 0
|
||||
| (1 << PWM1B)
|
||||
| (2 << COM1B0)
|
||||
;
|
||||
}
|
||||
|
||||
/// Convert an intensity to PWM.
|
||||
const uint8_t Chroma::ciel8[] = {
|
||||
0, 0, 0, 0, 0, 1, 1, 1,
|
||||
1, 1, 1, 2, 2, 2, 2, 3,
|
||||
3, 3, 4, 4, 5, 5, 6, 7,
|
||||
7, 8, 9, 10, 11, 12, 13, 14,
|
||||
16, 17, 19, 21, 23, 25, 27, 30,
|
||||
33, 36, 39, 43, 47, 51, 56, 61,
|
||||
67, 73, 80, 88, 96, 105, 115, 125,
|
||||
137, 149, 163, 178, 195, 213, 233, 255,
|
||||
};
|
||||
|
||||
/// Clip a value to 0..1.
|
||||
uint8_t Chroma::clip(int16_t v)
|
||||
{
|
||||
if (v > One) {
|
||||
return One;
|
||||
} else if (v < 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return (uint8_t)v;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clip and correct a intensity.
|
||||
uint8_t Chroma::correct(int16_t v)
|
||||
{
|
||||
return ciel8[clip(v)/(Scale/64)];
|
||||
}
|
||||
|
||||
/// Convert HSV to RGB.
|
||||
/// hdash is 0..6.
|
||||
void Chroma::hsv_to_rgb(int16_t hdash, RGB* rgb)
|
||||
{
|
||||
const int16_t xs = One;
|
||||
const int16_t xv = One;
|
||||
int16_t chroma = xs * xv / Scale;
|
||||
int16_t x = chroma * (1*Scale - abs((hdash % (Scale*2)) - Scale)) / Scale;
|
||||
|
||||
if (hdash < 1*Scale)
|
||||
{
|
||||
*rgb = { chroma, x, 0 };
|
||||
} else if (hdash < 2*Scale) {
|
||||
*rgb = { x, chroma, 0 };
|
||||
} else if (hdash < 3*Scale) {
|
||||
*rgb = { 0, chroma, x };
|
||||
} else if (hdash < 4*Scale) {
|
||||
*rgb = { 0, x, chroma };
|
||||
} else if (hdash < 5*Scale) {
|
||||
*rgb = { x, 0, chroma };
|
||||
} else {
|
||||
*rgb = { chroma, 0, x };
|
||||
}
|
||||
|
||||
int16_t min = xv - chroma;
|
||||
|
||||
rgb->r += min;
|
||||
rgb->g += min;
|
||||
rgb->b += min;
|
||||
}
|
||||
|
||||
/// Delay the given number of ticks.
|
||||
void Chroma::delay(uint8_t count)
|
||||
{
|
||||
uint8_t now = ticks;
|
||||
|
||||
while (count != 0) {
|
||||
while (now == ticks) {
|
||||
sleep_mode();
|
||||
}
|
||||
|
||||
count--;
|
||||
now++;
|
||||
}
|
||||
}
|
||||
|
||||
void Chroma::run()
|
||||
{
|
||||
sei();
|
||||
|
||||
for (;;) {
|
||||
int16_t step = 1;
|
||||
|
||||
// Cycle around the hues.
|
||||
for (int16_t h = 0; h < Scale*(360/60); h += step) {
|
||||
RGB rgb;
|
||||
hsv_to_rgb(h, &rgb);
|
||||
OCR0A = correct(rgb.r);
|
||||
OCR0B = correct(rgb.g);
|
||||
OCR1B = 255 - correct(rgb.b);
|
||||
|
||||
delay(5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ISR(TIMER0_OVF_vect)
|
||||
{
|
||||
Chroma::ticks++;
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
Chroma::init();
|
||||
Chroma::run();
|
||||
|
||||
return 0;
|
||||
}
|
13
cmulti/example.cc
Normal file
13
cmulti/example.cc
Normal file
|
@ -0,0 +1,13 @@
|
|||
#include <stdint.h>
|
||||
|
||||
void serializeint16(int16_t v);
|
||||
void serializeint8(int8_t v);
|
||||
|
||||
#include "make_struct.h"
|
||||
#include "servo_param.h"
|
||||
|
||||
#include "make_serializer.h"
|
||||
#include "servo_param.h"
|
||||
|
||||
#include "make_init.h"
|
||||
#include "servo_param.h"
|
8
cmulti/make_init.h
Normal file
8
cmulti/make_init.h
Normal file
|
@ -0,0 +1,8 @@
|
|||
#undef NAME
|
||||
#undef FIELD
|
||||
#undef END
|
||||
|
||||
#define NAME(x) void reset##x(x##_t* value) { \
|
||||
*value = {
|
||||
#define FIELD(type, name, def, doc) .name = def,
|
||||
#define END(x) }; }
|
7
cmulti/make_serializer.h
Normal file
7
cmulti/make_serializer.h
Normal file
|
@ -0,0 +1,7 @@
|
|||
#undef NAME
|
||||
#undef FIELD
|
||||
#undef END
|
||||
|
||||
#define NAME(x) void serialize##x(const x##_t* value) {
|
||||
#define FIELD(type, name, def, doc) serialize##type(value->name);
|
||||
#define END(x) }
|
7
cmulti/make_struct.h
Normal file
7
cmulti/make_struct.h
Normal file
|
@ -0,0 +1,7 @@
|
|||
#undef NAME
|
||||
#undef FIELD
|
||||
#undef END
|
||||
|
||||
#define NAME(x) typedef struct x##_s {
|
||||
#define FIELD(type, name, def, doc) type##_t name;
|
||||
#define END(x) } x##_t;
|
6
cmulti/servo_param.h
Normal file
6
cmulti/servo_param.h
Normal file
|
@ -0,0 +1,6 @@
|
|||
NAME(servoParam)
|
||||
FIELD(int16, min, 1000, "Pulse for minimum travel")
|
||||
FIELD(int16, max, 2000, "Pulse width for maximum travel")
|
||||
FIELD(int16, middle, 1500, "Pulse width for middle position")
|
||||
FIELD(int8, rate, 10, "Travel rate in us/s")
|
||||
END(servoParam)
|
|
@ -1,14 +0,0 @@
|
|||
#!/bin/sh /etc/rc.common
|
||||
# OpenWRT compatible init script.
|
||||
USE_PROCD=1
|
||||
START=95
|
||||
STOP=01
|
||||
|
||||
start_service() {
|
||||
procd_open_instance
|
||||
procd_set_param command ujail -n mqtt_rewrite -R / -o -U nobody -- /usr/bin/mqtt_rewrite
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
}
|
152
mqtt_rewrite
152
mqtt_rewrite
|
@ -1,152 +0,0 @@
|
|||
#!/usr/bin/micropython
|
||||
"""Accepts MQTT connections and rewrites the PUBLISH messages before forwarding on.
|
||||
|
||||
This can be used to fix the invalid packed ID sent by Solax inverters."""
|
||||
import asyncio
|
||||
import ssl
|
||||
import socket
|
||||
import sys
|
||||
from errno import EINPROGRESS
|
||||
|
||||
try:
|
||||
import asyncio.core
|
||||
import asyncio.stream
|
||||
|
||||
IS_MICROPYTHON = True
|
||||
except:
|
||||
IS_MICROPYTHON = False
|
||||
|
||||
try:
|
||||
from typing import Optional
|
||||
except:
|
||||
# MicroPython doesn't have `typing`.
|
||||
pass
|
||||
|
||||
|
||||
def _log(msg: str) -> None:
|
||||
print(msg)
|
||||
|
||||
|
||||
def _open_micropython_ssl_connection(host: str, port: int):
|
||||
"""MicroPython 1.21 implementation of asyncio.open_connection."""
|
||||
ai = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)[0]
|
||||
s = socket.socket(ai[0], ai[1], ai[2])
|
||||
s.setblocking(False)
|
||||
try:
|
||||
s.connect(ai[-1])
|
||||
except OSError as er:
|
||||
if er.errno != EINPROGRESS:
|
||||
raise er
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
s = context.wrap_socket(s, server_hostname=host, do_handshake_on_connect=False)
|
||||
s.setblocking(False)
|
||||
ss = asyncio.stream.Stream(s)
|
||||
yield asyncio.core._io_queue.queue_write(s)
|
||||
return ss, ss
|
||||
|
||||
|
||||
async def _open_ssl_connection(host: str, port: int):
|
||||
if IS_MICROPYTHON:
|
||||
return await _open_micropython_ssl_connection(host, port)
|
||||
else:
|
||||
return await asyncio.open_connection(host, port, ssl=True)
|
||||
|
||||
|
||||
async def _try_get(reader, writer) -> Optional[int]:
|
||||
"""Returns the next byte from `reader`, or None if closed."""
|
||||
got = await reader.read(1)
|
||||
if not got:
|
||||
return None
|
||||
writer.write(got)
|
||||
return got[0]
|
||||
|
||||
|
||||
async def _get(reader, writer) -> int:
|
||||
"""Returns the next byte from `reader`, or Exception on closed."""
|
||||
got = await _try_get(reader, writer)
|
||||
if got is None:
|
||||
raise EOFError("Unexpected EOF")
|
||||
return got
|
||||
|
||||
|
||||
async def _get_length(reader, writer) -> int:
|
||||
"""Returns the encoded length, or Exception on closed or oversize."""
|
||||
lsb = await _get(reader, writer)
|
||||
if (lsb & 0x80) == 0:
|
||||
return lsb
|
||||
|
||||
msb = await _get(reader, writer)
|
||||
if (msb & 0x80) != 0:
|
||||
# Protect the system by faulting on messages greater than ~16 KiB.
|
||||
raise OSError("Packet is too long")
|
||||
return (msb << 7) | (lsb & 0x7F)
|
||||
|
||||
|
||||
def _rewrite_publish(message: bytearray) -> None:
|
||||
"""Rewrites a publish message by fixing the packet offset if invalid."""
|
||||
topic_length = (message[0] << 8) | message[1]
|
||||
packet_id_offset = topic_length + 2
|
||||
|
||||
if message[packet_id_offset] == 0 and message[packet_id_offset + 1] == 0:
|
||||
message[packet_id_offset] = message[0]
|
||||
message[packet_id_offset + 1] = message[1]
|
||||
|
||||
|
||||
async def _rewrite(direction: str, reader, writer) -> None:
|
||||
"""Copies messages from `reader` to `writer` with rewrites."""
|
||||
while True:
|
||||
packet_type = await _try_get(reader, writer)
|
||||
if packet_type is None:
|
||||
break
|
||||
|
||||
length = await _get_length(reader, writer)
|
||||
_log(f"{direction} type={packet_type:x} length={length}")
|
||||
payload = bytearray(await reader.readexactly(length))
|
||||
qos = (packet_type >> 1) & 0x03
|
||||
if (packet_type & 0xF0) == 0x30 and qos > 0:
|
||||
_rewrite_publish(payload)
|
||||
|
||||
writer.write(payload)
|
||||
await writer.drain()
|
||||
|
||||
|
||||
async def _client(reader, writer, upstream: str, upstream_port: int) -> None:
|
||||
try:
|
||||
ureader, uwriter = await _open_ssl_connection(upstream, upstream_port)
|
||||
try:
|
||||
await asyncio.gather(
|
||||
asyncio.create_task(_rewrite("<", ureader, writer)),
|
||||
asyncio.create_task(_rewrite(">", reader, uwriter)),
|
||||
)
|
||||
finally:
|
||||
uwriter.close()
|
||||
except Exception as ex:
|
||||
_log(f"Giving up due to exception: {type(ex)} {ex}")
|
||||
finally:
|
||||
writer.close()
|
||||
|
||||
|
||||
async def _serve(
|
||||
listen: str, listen_port: int, upstream: str, upstream_port: int
|
||||
) -> None:
|
||||
async def _wrapper(reader, writer):
|
||||
return await _client(reader, writer, upstream, upstream_port)
|
||||
|
||||
server = await asyncio.start_server(_wrapper, listen, listen_port)
|
||||
async with server:
|
||||
await server.wait_closed()
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:] + [None] * 4
|
||||
upstream = args[0] or "localhost"
|
||||
upstream_port = int(args[1] or 8883)
|
||||
listen = args[2] or "0.0.0.0"
|
||||
listen_port = int(args[3] or 2901)
|
||||
|
||||
asyncio.run(_serve(listen, listen_port, upstream, upstream_port))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
22
onedigital/Makefile
Normal file
22
onedigital/Makefile
Normal file
|
@ -0,0 +1,22 @@
|
|||
PDFS = $(wildcard *.pdf)
|
||||
PFS = $(wildcard export*.pf)
|
||||
CSVS = $(PDFS:%.pdf=%.csv) $(PFS:%.pf=%.csv)
|
||||
|
||||
all: $(CSVS) all.csv
|
||||
|
||||
all.csv: $(CSVS)
|
||||
cat $^ |sort > $@
|
||||
|
||||
visebpp_%.csv: visebpp_%.pdftables onecsv.py
|
||||
python3 onecsv.py --out $@ --csv $<
|
||||
|
||||
%.csv: %.pf pfcsv.py
|
||||
python3 pfcsv.py --out $@ --csv $<
|
||||
|
||||
502%.csv: 502%.pdftables cembracsv.py
|
||||
python3 cembracsv.py --out $@ --csv $<
|
||||
|
||||
%.pdftables: %.pdf pdf2csv.py
|
||||
python3 pdf2csv.py --out $@ --pdf $<
|
||||
|
||||
.PRECIOUS: %.pdftables
|
79
onedigital/cembracsv.py
Normal file
79
onedigital/cembracsv.py
Normal file
|
@ -0,0 +1,79 @@
|
|||
import csv as pcsv
|
||||
import dataclasses
|
||||
from typing import Optional, Tuple
|
||||
import re
|
||||
import datetime
|
||||
|
||||
import click
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Entry:
|
||||
date: str
|
||||
account: str
|
||||
memo: str
|
||||
location: str
|
||||
kind: str
|
||||
amount: float
|
||||
|
||||
|
||||
def parse_date(text: str) -> datetime.date:
|
||||
d, m, y = (int(x) for x in text.split('.'))
|
||||
return datetime.date(year=y, month=m, day=d)
|
||||
|
||||
|
||||
def parse_memo(memo: str) -> Tuple[str, str, str]:
|
||||
location = ''
|
||||
kind = ''
|
||||
memo = memo.split('\n')[0]
|
||||
|
||||
match = re.match(r'(.+) ([A-Z]{3})$', memo)
|
||||
if match:
|
||||
memo = match.group(1)
|
||||
location = match.group(2)
|
||||
|
||||
return memo, location.strip(), kind
|
||||
|
||||
|
||||
def parse(line: tuple) -> Optional[Entry]:
|
||||
# 19.10.2021,19.10.2021,Nintendo CD598510225 Frankfurt am DEU,,5.60
|
||||
if len(line) < 4 or not line[-1]:
|
||||
return None
|
||||
|
||||
if not re.match(r'\d+\.\d+\.\d+', line[0]):
|
||||
return None
|
||||
|
||||
date = parse_date(line[0])
|
||||
memo = line[2]
|
||||
memo, location, kind = parse_memo(memo)
|
||||
|
||||
amount = float(line[-1].replace("'", ""))
|
||||
|
||||
return Entry(date=str(date),
|
||||
account="cembra",
|
||||
memo=memo,
|
||||
location=location,
|
||||
kind=kind,
|
||||
amount=amount)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--out', required=True, type=str)
|
||||
@click.option('--csv', required=True, type=str)
|
||||
def extract(out: str, csv: str):
|
||||
entries = []
|
||||
|
||||
with open(csv) as f:
|
||||
for line in pcsv.reader(f):
|
||||
entry = parse(line)
|
||||
if entry:
|
||||
entries.append(entry)
|
||||
|
||||
with open(out, 'w') as f:
|
||||
writer = pcsv.writer(f)
|
||||
for entry in entries:
|
||||
writer.writerow(dataclasses.astuple(entry))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
extract()
|
89
onedigital/onecsv.py
Normal file
89
onedigital/onecsv.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
import csv as pcsv
|
||||
import dataclasses
|
||||
from typing import Optional, Tuple
|
||||
import re
|
||||
import datetime
|
||||
|
||||
import click
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Entry:
|
||||
date: str
|
||||
account: str
|
||||
memo: str
|
||||
location: str
|
||||
kind: str
|
||||
amount: float
|
||||
|
||||
|
||||
def parse_date(text: str) -> datetime.date:
|
||||
d, m, y = (int(x) for x in text.split('.'))
|
||||
return datetime.date(year=2000 + y, month=m, day=d)
|
||||
|
||||
|
||||
def parse_memo(memo: str) -> Tuple[str, str, str]:
|
||||
parts = memo.split('\n')
|
||||
if len(parts) == 1:
|
||||
memo, kind = parts[0], ''
|
||||
else:
|
||||
memo, kind = parts[0], parts[1]
|
||||
parts = memo.rsplit(',', maxsplit=1)
|
||||
if len(parts) == 1:
|
||||
memo, location = parts[0], ''
|
||||
else:
|
||||
memo, location = parts
|
||||
return memo, location.strip(), kind
|
||||
|
||||
|
||||
def parse(line: tuple) -> Optional[Entry]:
|
||||
# ['15.07.22 18.07.22 NYA*Arena Cinemas AG, 433050453 CH\nLebensmittel, Spezialgeschäfte', '', '', '', '', '', '4.00']
|
||||
if len(line) < 4 or not line[-1]:
|
||||
return None
|
||||
|
||||
if not re.match(r'\d+\.\d+\.\d+ \d+', line[0]):
|
||||
return None
|
||||
|
||||
parts = line[0].split(maxsplit=2)
|
||||
if len(parts) == 3:
|
||||
date = parse_date(parts[0])
|
||||
memo = parts[-1]
|
||||
else:
|
||||
date = parse_date(parts[0])
|
||||
memo = line[2]
|
||||
memo, location, kind = parse_memo(memo)
|
||||
|
||||
parts = line[-1].split(' ', maxsplit=1)
|
||||
amount = float(parts[0].replace("'", ''))
|
||||
if len(parts) == 2:
|
||||
assert parts[-1] == '-'
|
||||
amount = -amount
|
||||
|
||||
return Entry(date=str(date),
|
||||
account="one",
|
||||
memo=memo,
|
||||
location=location,
|
||||
kind=kind,
|
||||
amount=amount)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--out', required=True, type=str)
|
||||
@click.option('--csv', required=True, type=str)
|
||||
def extract(out: str, csv: str):
|
||||
entries = []
|
||||
|
||||
with open(csv) as f:
|
||||
for line in pcsv.reader(f):
|
||||
entry = parse(line)
|
||||
if entry:
|
||||
entries.append(entry)
|
||||
|
||||
with open(out, 'w') as f:
|
||||
writer = pcsv.writer(f)
|
||||
for entry in entries:
|
||||
writer.writerow(dataclasses.astuple(entry))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
extract()
|
29
onedigital/pdf2csv.py
Normal file
29
onedigital/pdf2csv.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
import sys
|
||||
|
||||
import click
|
||||
import requests
|
||||
|
||||
import secrets
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--out', required=True, type=str)
|
||||
@click.option('--pdf', required=True, type=str)
|
||||
def extract(out: str, pdf: str):
|
||||
params = {
|
||||
'key': secrets.PDFTABLES_KEY,
|
||||
'format': 'csv',
|
||||
}
|
||||
with open(pdf, 'rb') as f:
|
||||
files = {
|
||||
'f': ('file.pdf', f.read()),
|
||||
}
|
||||
body = requests.post('https://pdftables.com/api',
|
||||
params=params,
|
||||
files=files)
|
||||
with open(out, 'w') as f:
|
||||
f.write(body.text)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
extract()
|
127
onedigital/pfcsv.py
Normal file
127
onedigital/pfcsv.py
Normal file
|
@ -0,0 +1,127 @@
|
|||
import csv as pcsv
|
||||
import dataclasses
|
||||
from typing import Optional, Tuple
|
||||
import re
|
||||
import datetime
|
||||
|
||||
import click
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Entry:
|
||||
date: str
|
||||
account: str
|
||||
memo: str
|
||||
location: str
|
||||
kind: str
|
||||
amount: float
|
||||
|
||||
|
||||
def parse_dict(filename: str):
|
||||
words = {}
|
||||
with open(filename) as f:
|
||||
for line in f:
|
||||
for word in line.strip().split():
|
||||
words[word.lower()] = word
|
||||
for word in ('AG', 'TWINT', 'SBB'):
|
||||
words[word.lower()] = word
|
||||
|
||||
return words
|
||||
|
||||
|
||||
WORDS = parse_dict('/usr/share/dict/words')
|
||||
|
||||
|
||||
def fix_case(line: str) -> str:
|
||||
words = []
|
||||
for i, word in enumerate(line.split()):
|
||||
word = WORDS.get(word.lower(), word.lower())
|
||||
if i == 0:
|
||||
word = word[0].upper() + word[1:]
|
||||
words.append(word)
|
||||
return ' '.join(words)
|
||||
|
||||
|
||||
def parse_date(text: str) -> datetime.date:
|
||||
d, m, y = (int(x) for x in text.split('.'))
|
||||
return datetime.date(year=y, month=m, day=d)
|
||||
|
||||
|
||||
def parse_memo(memo: str) -> Tuple[str, str, str]:
|
||||
location = ''
|
||||
kind = ''
|
||||
|
||||
memo = memo.strip()
|
||||
|
||||
match = re.match(r'^.+CARD NO. XX\S+\s+(.+)', memo)
|
||||
if match:
|
||||
memo = match.group(1)
|
||||
|
||||
match = re.match(r'TWINT PURCHASE/SERVICE FROM \S+ FROM TELEPHONE NO. \S+\s+(.+)', memo)
|
||||
if match:
|
||||
memo = match.group(1)
|
||||
|
||||
match = re.match(r'(DEBIT|CREDIT) CH\d+\s+(.+)', memo)
|
||||
if match:
|
||||
memo = match.group(2)
|
||||
|
||||
memo = fix_case(memo)
|
||||
|
||||
match = re.match(r'(.+) (\w+) (Switzerland|France)$', memo)
|
||||
if match:
|
||||
memo, city, country = match.groups()
|
||||
location = f'{city} {country}'
|
||||
|
||||
return memo, location, kind
|
||||
|
||||
|
||||
def parse(line: tuple) -> Optional[Entry]:
|
||||
# 13.07.2024;Entry;"TWINT PURCHASE/SERVICE FROM 12.07.2024 FROM TELEPHONE NO. +41792456887 MCDONALD'S ZÜRICH SIHLCITY ZUERICH";;-9.50;;
|
||||
if len(line) < 5 or not line[-1]:
|
||||
return None
|
||||
|
||||
if not re.match(r'\d+\.\d+\.\d+', line[0]):
|
||||
return None
|
||||
|
||||
date = parse_date(line[0])
|
||||
memo = line[2]
|
||||
memo, location, kind = parse_memo(memo)
|
||||
|
||||
if line[3]:
|
||||
amount = float(line[3].replace("'", ""))
|
||||
else:
|
||||
amount = -float(line[4].replace("'", ""))
|
||||
|
||||
if not kind:
|
||||
kind = line[-1]
|
||||
|
||||
return Entry(date=str(date),
|
||||
account="pf",
|
||||
memo=memo,
|
||||
location=location,
|
||||
kind=kind,
|
||||
amount=amount)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--out', required=True, type=str)
|
||||
@click.option('--csv', required=True, type=str)
|
||||
def extract(out: str, csv: str):
|
||||
entries = []
|
||||
|
||||
with open(csv) as f:
|
||||
dialect = pcsv.Sniffer().sniff(f.read(1024), ';')
|
||||
f.seek(0)
|
||||
for line in pcsv.reader(f, dialect=dialect):
|
||||
entry = parse(line)
|
||||
if entry:
|
||||
entries.append(entry)
|
||||
|
||||
with open(out, 'w') as f:
|
||||
writer = pcsv.writer(f)
|
||||
for entry in entries:
|
||||
writer.writerow(dataclasses.astuple(entry))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
extract()
|
BIN
painter/images/car-128.png
Normal file
BIN
painter/images/car-128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 434 B |
BIN
painter/images/car-256.png
Normal file
BIN
painter/images/car-256.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 784 B |
BIN
painter/images/car-64.png
Normal file
BIN
painter/images/car-64.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 296 B |
BIN
painter/images/girl19.png
Normal file
BIN
painter/images/girl19.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 263 B |
BIN
painter/images/space_invader.png
Normal file
BIN
painter/images/space_invader.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 200 B |
210
painter/painter.py
Normal file
210
painter/painter.py
Normal file
|
@ -0,0 +1,210 @@
|
|||
"""Converts a bitmap image into a minimal number of covering
|
||||
rectangles.
|
||||
|
||||
Mainly plays with different methods.
|
||||
"""
|
||||
|
||||
# python painter.py images/*.png
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
import collections
|
||||
import time
|
||||
|
||||
import numpy
|
||||
import scipy.misc
|
||||
|
||||
# A rectangle.
|
||||
Rect = collections.namedtuple('Rect', 'x y w h')
|
||||
|
||||
# Chracters to use when printing the image to the screen.
|
||||
EXT = range(ord('1'), ord('9') + 1) \
|
||||
+ range(ord('a'), ord('z') + 1) \
|
||||
+ range(ord('A'), ord('Z') + 1)
|
||||
CHARS = [ord('.'), ord('#')] + EXT*20
|
||||
|
||||
|
||||
def parse(filename):
|
||||
"""Parse a image into a (0, 1) numpy array."""
|
||||
img = scipy.misc.imread(filename)
|
||||
img = numpy.clip(img, 0, 1)
|
||||
return img ^ 1
|
||||
|
||||
|
||||
def render(shape, rects, with_ids=False):
|
||||
"""Render rectangles into a new array of the given shape."""
|
||||
img = numpy.zeros(shape, numpy.uint16)
|
||||
|
||||
for i, rect in enumerate(rects):
|
||||
for y in range(rect.y, rect.y + rect.h):
|
||||
for x in range(rect.x, rect.x + rect.w):
|
||||
if with_ids:
|
||||
img[y][x] = i + 2
|
||||
else:
|
||||
img[y][x] = 1
|
||||
return img
|
||||
|
||||
def show(img):
|
||||
"""Print an image to the console."""
|
||||
for row in img:
|
||||
print(''.join(chr(CHARS[x]) for x in row))
|
||||
|
||||
|
||||
def pointwise(img):
|
||||
"""Method where each point is a rectangle."""
|
||||
for y, row in enumerate(img):
|
||||
for x, pix in enumerate(row):
|
||||
if pix:
|
||||
yield Rect(x, y, 1, 1)
|
||||
|
||||
def rowwise(img):
|
||||
"""Method where rectangles are one row high."""
|
||||
for y, row in enumerate(img):
|
||||
start = None
|
||||
for x, pix in enumerate(row):
|
||||
if pix:
|
||||
if start is None:
|
||||
start = x
|
||||
else:
|
||||
if start is not None:
|
||||
yield Rect(start, y, x - start, 1)
|
||||
start = None
|
||||
if start is not None:
|
||||
yield Rect(start, y, x - start + 1, 1)
|
||||
|
||||
def rotate(img, fun):
|
||||
"""Rotate an image and rotate the final rectangles."""
|
||||
h, w = img.shape
|
||||
for rect in fun(numpy.rot90(img)):
|
||||
yield Rect(w - rect.y - 1, rect.x, rect.h, rect.w)
|
||||
|
||||
def colwise(img):
|
||||
"""Method where rectangles are one column wide."""
|
||||
return list(rotate(img, rowwise))
|
||||
|
||||
def mergeup(rects):
|
||||
"""Merge a rectangle into the identical width one below it."""
|
||||
# TODO(michaelh): ugly.
|
||||
while True:
|
||||
out = []
|
||||
merged = set()
|
||||
rects.sort(key=lambda x: x.y)
|
||||
|
||||
for rect in rects:
|
||||
if rect in merged:
|
||||
continue
|
||||
|
||||
top = rect.y + rect.h
|
||||
below = [x for x in rects if x.y == top]
|
||||
matches = [x for x in below if x.x == rect.x and x.w == rect.w]
|
||||
|
||||
if matches:
|
||||
match = matches[0]
|
||||
merged.add(match)
|
||||
out.append(Rect(rect.x, rect.y, rect.w, rect.h + match.h))
|
||||
else:
|
||||
out.append(rect)
|
||||
if not merged:
|
||||
return out
|
||||
rects = out
|
||||
|
||||
def mergeleft(rects):
|
||||
"""Merge a rectangle into the identical height one beside it."""
|
||||
# TODO(michaelh): merge with mergeup().
|
||||
|
||||
while True:
|
||||
out = []
|
||||
merged = set()
|
||||
rects.sort(key=lambda x: x.x)
|
||||
|
||||
for rect in rects:
|
||||
if rect in merged:
|
||||
continue
|
||||
|
||||
edge = rect.x + rect.w
|
||||
adjacent = [x for x in rects if x.x == edge]
|
||||
matches = [x for x in adjacent if x.y == rect.y and x.h == rect.h]
|
||||
|
||||
if matches:
|
||||
match = matches[0]
|
||||
merged.add(match)
|
||||
out.append(Rect(rect.x, rect.y, rect.w + match.w, rect.h))
|
||||
else:
|
||||
out.append(rect)
|
||||
if not merged:
|
||||
return out
|
||||
rects = out
|
||||
|
||||
|
||||
def rowcolwise(img):
|
||||
"""Method that picks the best of rowwise and colwise."""
|
||||
rows = list(rowwise(img))
|
||||
cols = list(colwise(img))
|
||||
return rows if len(rows) < len(cols) else cols
|
||||
|
||||
def mergerowcolwise(img):
|
||||
"""Method that picks the best of the merged rowwise and colwise."""
|
||||
rows = mergeup(list(rowwise(img)))
|
||||
cols = mergeleft(list(colwise(img)))
|
||||
return rows if len(rows) < len(cols) else cols
|
||||
|
||||
def bothwise(img):
|
||||
"""Method that combines row and col wise, then drops rectangles
|
||||
that are completly covered by larger rectangles.
|
||||
"""
|
||||
rows = list(rowwise(img))
|
||||
cols = list(colwise(img))
|
||||
rects = rows + cols
|
||||
# Sort by area. Smaller are less likely to contribute.
|
||||
rects.sort(key=lambda x: x.w*x.h)
|
||||
drop = set()
|
||||
|
||||
for rect in rects:
|
||||
others = [x for x in rects if x != rect and x not in drop]
|
||||
got = render(img.shape, others)
|
||||
|
||||
if not (got ^ img).any():
|
||||
# Dropping this rect didn't change the image.
|
||||
drop.add(rect)
|
||||
|
||||
rects = [x for x in rects if x not in drop]
|
||||
return mergeleft(mergeup(rects))
|
||||
|
||||
def main():
|
||||
# All methods to try.
|
||||
methods = [
|
||||
pointwise,
|
||||
rowwise,
|
||||
colwise,
|
||||
rowcolwise,
|
||||
mergerowcolwise,
|
||||
bothwise,
|
||||
]
|
||||
|
||||
for filename in sys.argv[1:]:
|
||||
img = parse(filename)
|
||||
best = None
|
||||
|
||||
print('{}:'.format(filename))
|
||||
for method in methods:
|
||||
start = time.clock()
|
||||
scan = list(method(img))
|
||||
elapsed = time.clock() - start
|
||||
|
||||
name = method.__name__
|
||||
print(' {} -> {} rects in {:.3f} s'.format(name, len(scan), elapsed))
|
||||
|
||||
rendered = render(img.shape, scan)
|
||||
diff = img ^ rendered
|
||||
|
||||
if diff.any():
|
||||
print('Render error!')
|
||||
else:
|
||||
if best is None or len(scan) <= len(best):
|
||||
best = scan
|
||||
# Show the best.
|
||||
show(render(img.shape, best, True))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
10
sudoku/333597492-v3-26-L1.board
Normal file
10
sudoku/333597492-v3-26-L1.board
Normal file
|
@ -0,0 +1,10 @@
|
|||
# http://kjell.haxx.se/sudoku/
|
||||
......635
|
||||
.7.56....
|
||||
3.....84.
|
||||
..327.1.6
|
||||
.........
|
||||
52.8.6.9.
|
||||
.61...5..
|
||||
......4..
|
||||
...3.4.6.
|
10
sudoku/617498478-v3-17-L5.board
Normal file
10
sudoku/617498478-v3-17-L5.board
Normal file
|
@ -0,0 +1,10 @@
|
|||
# http://kjell.haxx.se/sudoku/
|
||||
.5.4.....
|
||||
7.....28.
|
||||
.......1.
|
||||
....28...
|
||||
....1....
|
||||
.4.....9.
|
||||
8.......7
|
||||
1...7....
|
||||
...3....5
|
9
sudoku/Makefile
Normal file
9
sudoku/Makefile
Normal file
|
@ -0,0 +1,9 @@
|
|||
CXXFLAGS = -O3 -std=c++0x
|
||||
BOARDS = $(wildcard *.board)
|
||||
|
||||
all: sudoku
|
||||
|
||||
check: $(BOARDS:%.board=%.run)
|
||||
|
||||
%.run: %.board sudoku
|
||||
./sudoku < $<
|
14
sudoku/README
Normal file
14
sudoku/README
Normal file
|
@ -0,0 +1,14 @@
|
|||
Sudoku Solver
|
||||
=============
|
||||
Done for the Olimex Weekend Programming Challenge #28 [1].
|
||||
|
||||
Build: make
|
||||
|
||||
Run: make test
|
||||
|
||||
Benchmarks:
|
||||
Feroceon 88FR131 / 2.0 GHz ARMv5 / GCC 4.6.3: 16.0 M/s
|
||||
i.MX6 Quad / 1.0 GHz Cortex-A9 / GCC 4.7.3: 14.4 M/s
|
||||
Core 2 P8600 / 2.4 GHz Core 2 / GCC 4.7.3: 54.2 M/s
|
||||
|
||||
[1] http://olimex.wordpress.com/2013/10/11/weekend-programming-challenge-week-28-sudoku-solver/
|
9
sudoku/basic.board
Normal file
9
sudoku/basic.board
Normal file
|
@ -0,0 +1,9 @@
|
|||
.....8..4
|
||||
.84.16...
|
||||
...5..1..
|
||||
1.38..9..
|
||||
6.8...4.3
|
||||
..2..95.1
|
||||
..7..2...
|
||||
...78.26.
|
||||
2..3.....
|
11
sudoku/hardest.board
Normal file
11
sudoku/hardest.board
Normal file
|
@ -0,0 +1,11 @@
|
|||
# "World's hardest sudoku: can you crack it?"
|
||||
# http://www.telegraph.co.uk/science/science-news/9359579/Worlds-hardest-sudoku-can-you-crack-it.html
|
||||
8........
|
||||
..36.....
|
||||
.7..9.2..
|
||||
.5...7...
|
||||
....457..
|
||||
...1...3.
|
||||
..1....68
|
||||
..85...1.
|
||||
.9....4..
|
192
sudoku/sudoku.cc
Normal file
192
sudoku/sudoku.cc
Normal file
|
@ -0,0 +1,192 @@
|
|||
// Sudoku solver
|
||||
// MIT license michaelh@juju.net.nz
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cctype>
|
||||
#include <cassert>
|
||||
#include <ctime>
|
||||
#include <cstdint>
|
||||
|
||||
// Uses sets to find out what's used and what the possibilities are.
|
||||
// Implemented as a bitmask so an empty cell has the value 0, a cell
|
||||
// with a 3 in contains 1 << 3, etc.
|
||||
|
||||
class Board
|
||||
{
|
||||
public:
|
||||
uint16_t cells[9][9];
|
||||
void Read();
|
||||
void Print() const;
|
||||
};
|
||||
|
||||
struct CellRef
|
||||
{
|
||||
int x;
|
||||
int y;
|
||||
};
|
||||
|
||||
class Stats
|
||||
{
|
||||
public:
|
||||
int solutions;
|
||||
|
||||
void Init();
|
||||
void Print();
|
||||
void Bump();
|
||||
|
||||
private:
|
||||
clock_t start;
|
||||
uint64_t tests;
|
||||
};
|
||||
|
||||
Stats stats;
|
||||
|
||||
static int FFS(int v)
|
||||
{
|
||||
return sizeof(v)*8 - 1 - __builtin_clz(v);
|
||||
}
|
||||
|
||||
void Stats::Init()
|
||||
{
|
||||
solutions = 0;
|
||||
tests = 0;
|
||||
start = clock();
|
||||
}
|
||||
|
||||
void Stats::Print()
|
||||
{
|
||||
double elapsed = (clock() - stats.start) / double(CLOCKS_PER_SEC);
|
||||
printf("%llu tests %d solutions %.3f s (%.1f M/s)\n",
|
||||
stats.tests, stats.solutions,
|
||||
elapsed, stats.tests / elapsed / 1e6);
|
||||
}
|
||||
|
||||
void Stats::Bump()
|
||||
{
|
||||
stats.tests++;
|
||||
|
||||
if ((stats.tests & 0x3ffffff) == 0) {
|
||||
stats.Print();
|
||||
}
|
||||
}
|
||||
|
||||
void Board::Read()
|
||||
{
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int ch;
|
||||
|
||||
memset(cells, 0, sizeof(cells));
|
||||
|
||||
while ((ch = fgetc(stdin)) != EOF) {
|
||||
if (ch == '#') {
|
||||
// Drop comment lines (kind of)
|
||||
while ((ch = fgetc(stdin)) != EOF && ch != '\n') {
|
||||
}
|
||||
} else if (ch == '\n') {
|
||||
x = 0;
|
||||
y++;
|
||||
} else if (ch == '\r') {
|
||||
// Drop
|
||||
} else if (ch == '.') {
|
||||
cells[y][x++] = 0;
|
||||
} else if (isdigit(ch)) {
|
||||
cells[y][x++] = 1 << (ch - '0');
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Board::Print() const
|
||||
{
|
||||
for (int y = 0; y < 9; y++) {
|
||||
for (int x = 0; x < 9; x++) {
|
||||
if (cells[y][x] == 0) {
|
||||
putchar('.');
|
||||
} else {
|
||||
putchar('0'+ FFS(cells[y][x]));
|
||||
}
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
// Find all of the blank cells. Terminate the list with -1.
|
||||
static void FindBlanks(const Board& board, CellRef* prefs)
|
||||
{
|
||||
for (int y = 0; y < 9; y++) {
|
||||
for (int x = 0; x < 9; x++) {
|
||||
if (board.cells[y][x] == 0) {
|
||||
prefs->x = x;
|
||||
prefs->y = y;
|
||||
prefs++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prefs->x = -1;
|
||||
}
|
||||
|
||||
static void OnSolution(const Board& board)
|
||||
{
|
||||
stats.solutions++;
|
||||
|
||||
// Print the first solution and keep going.
|
||||
if (stats.solutions == 1) {
|
||||
board.Print();
|
||||
}
|
||||
}
|
||||
|
||||
static void Solve(Board& board, const CellRef* pref)
|
||||
{
|
||||
// Mask for 1 to 9
|
||||
const int All = (1 << 10) - 1 - 1;
|
||||
|
||||
int x = pref->x;
|
||||
int y = pref->y;
|
||||
|
||||
if (x == -1) {
|
||||
OnSolution(board);
|
||||
}
|
||||
else {
|
||||
int present = 0;
|
||||
// Scan across the row and down the column to see which digits
|
||||
// are already used.
|
||||
for (int i = 0; i < 9; i++) {
|
||||
present |= board.cells[y][i];
|
||||
present |= board.cells[i][x];
|
||||
}
|
||||
// Calculate the unused values.
|
||||
int possibles = (present & All) ^ All;
|
||||
|
||||
while (possibles != 0) {
|
||||
// Find the next and mark it as done.
|
||||
int next = 1 << FFS(possibles);
|
||||
possibles &= ~next;
|
||||
stats.Bump();
|
||||
|
||||
// Solve for the next blank cell.
|
||||
board.cells[y][x] = next;
|
||||
Solve(board, pref+1);
|
||||
board.cells[y][x] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
Board board;
|
||||
CellRef refs[100];
|
||||
board.Read();
|
||||
board.Print();
|
||||
|
||||
stats.Init();
|
||||
|
||||
FindBlanks(board, refs);
|
||||
Solve(board, refs);
|
||||
stats.Print();
|
||||
|
||||
return 0;
|
||||
}
|
Loading…
Reference in a new issue