Gisle Aune
5 years ago
17 changed files with 698 additions and 209 deletions
-
21Dockerfile
-
56cmd/lucifer-server/main.go
-
78database/sqlite/group-repository.go
-
46database/sqlite/init.go
-
112database/sqlite/light-repository.go
-
2database/sqlite/user-repository.go
-
1go.mod
-
2go.sum
-
82internal/huecolor/convert.go
-
16internal/huecolor/convert_test.go
-
86internal/huecolor/gamut.go
-
17internal/huecolor/gamut_test.go
-
16light/driver.go
-
175light/hue/driver.go
-
60light/service.go
-
35models/group.go
-
102models/light.go
@ -0,0 +1,21 @@ |
|||
FROM golang:1.11 as build-server |
|||
WORKDIR /server |
|||
COPY . . |
|||
RUN go mod tidy |
|||
ENV CGO_ENABLED 1 |
|||
RUN go build -ldflags "-w -s" ./cmd/lucifer-server/ |
|||
|
|||
FROM node:10.15.1-alpine as build-ui |
|||
WORKDIR /ui |
|||
COPY ./webui /ui |
|||
RUN npm install |
|||
RUN npm run build |
|||
|
|||
FROM alpine:3.8 |
|||
RUN apk add --no-cache sqlite |
|||
COPY /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2 |
|||
RUN apk remove sqlite |
|||
COPY --from=build-server /server/lucifer-server /usr/local/bin/ |
|||
COPY --from=build-ui /ui/build /usr/local/share/lucifer-ui |
|||
|
|||
CMD ["/usr/local/bin/lucifer-server"] |
@ -0,0 +1,78 @@ |
|||
package sqlite |
|||
|
|||
import ( |
|||
"context" |
|||
|
|||
"golang.org/x/sync/errgroup" |
|||
|
|||
"git.aiterp.net/lucifer/lucifer/models" |
|||
) |
|||
|
|||
type groupRepository struct{} |
|||
|
|||
// GroupRepository is a sqlite datbase repository for the Group model.
|
|||
var GroupRepository = &groupRepository{} |
|||
|
|||
func (r *groupRepository) FindByID(ctx context.Context, id int) (models.Group, error) { |
|||
group := models.Group{} |
|||
err := db.GetContext(ctx, &group, "SELECT * FROM group WHERE id=?", id) |
|||
|
|||
return group, err |
|||
} |
|||
|
|||
func (r *groupRepository) FindByLight(ctx context.Context, light models.Light) (models.Group, error) { |
|||
group := models.Group{} |
|||
err := db.GetContext(ctx, &group, "SELECT g.id, g.name FROM `light` l JOIN `group` g ON g.id = l.group_id WHERE l.id = ?", light.ID) |
|||
|
|||
return group, err |
|||
} |
|||
|
|||
func (r *groupRepository) List(ctx context.Context) ([]models.Group, error) { |
|||
groups := make([]models.Group, 0, 16) |
|||
err := db.SelectContext(ctx, &groups, "SELECT * FROM group") |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
eg, egCtx := errgroup.WithContext(ctx) |
|||
for i := range groups { |
|||
group := &groups[i] |
|||
|
|||
eg.Go(func() error { |
|||
return db.SelectContext(egCtx, &group.Permissions, "SELECT * FROM group_permissions WHERE group_id=?", group.ID) |
|||
}) |
|||
} |
|||
|
|||
return groups, eg.Wait() |
|||
} |
|||
|
|||
func (r *groupRepository) ListByUser(ctx context.Context, user models.User) ([]models.Group, error) { |
|||
panic("not implemented") |
|||
} |
|||
|
|||
func (r *groupRepository) Insert(ctx context.Context, group models.Group) (models.Group, error) { |
|||
panic("not implemented") |
|||
} |
|||
|
|||
func (r *groupRepository) Update(ctx context.Context, group models.Group) error { |
|||
_, err := db.NamedExecContext(ctx, "UPDATE group SET name=:name WHERE id=:id", group) |
|||
return err |
|||
} |
|||
|
|||
func (r *groupRepository) UpdatePermissions(ctx context.Context, permission models.GroupPermission) error { |
|||
_, err := db.NamedExecContext(ctx, "REPLACE INTO group_permission(group_id, user_id, read, write, 'create', 'delete', manage) VALUES (:group_id, :user_id, :read, :write, :create, :delete, :manage)", permission) |
|||
return err |
|||
} |
|||
|
|||
func (r *groupRepository) Remove(ctx context.Context, group models.Group) error { |
|||
_, err := db.NamedExecContext(ctx, "DELETE FROM group WHERE id=:id LIMIT 1", group) |
|||
return err |
|||
} |
|||
|
|||
/* |
|||
|
|||
SELECT g.id, g.name FROM group_permission AS p |
|||
JOIN 'group' AS g ON p.group_id=g.id |
|||
WHERE p.user_id=? AND p.read=true; |
|||
|
|||
*/ |
@ -0,0 +1,82 @@ |
|||
package huecolor |
|||
|
|||
import ( |
|||
"math" |
|||
) |
|||
|
|||
// ConvertXY converts the RGB value to XY and brightness and snaps it to the color gamut.
|
|||
func ConvertXY(r, g, b float64) (x, y float64) { |
|||
x, y, z := convertXYZ(correctGamma(r, g, b)) |
|||
|
|||
return x / (x + y + z), y / (x + y + z) |
|||
} |
|||
|
|||
// ConvertRGB converts back the values for ConvertXY to RGB. This does not gurantee that
|
|||
// `ConvertRGB(ConvertXY(r, g, b))` returns the same `r`, `g` and `b` because of Gamut
|
|||
// clamping.
|
|||
func ConvertRGB(x, y, bri float64) (r, g, b float64) { |
|||
z := (1 - x - y) |
|||
y2 := bri |
|||
x2 := (y2 / y) * x |
|||
z2 := (y2 / y) * z |
|||
|
|||
r = x2*1.4628067 - y2*0.1840623 - z2*0.2743606 |
|||
g = -x2*0.5217933 + y2*1.4472381 + z2*0.0677227 |
|||
b = x2*0.0349342 - y2*0.0968930 + z2*1.2884099 |
|||
|
|||
return reverseGamma(r, g, b) |
|||
} |
|||
|
|||
// convertXYZ converts the RGB values to XYZ using the Wide RGB D65 conversion formula.
|
|||
func convertXYZ(r, g, b float64) (x, y, z float64) { |
|||
return r*0.649926 + g*0.103455 + b*0.197109, |
|||
r*0.234327 + g*0.743075 + b*0.022598, |
|||
r*0.0000000 + g*0.053077 + b*1.035763 |
|||
} |
|||
|
|||
// correctGamma applies a gamma correction to the RGB values, which makes the color
|
|||
// more vivid and more the like the color displayed on the screen of your device.
|
|||
func correctGamma(r, g, b float64) (r2, g2, b2 float64) { |
|||
if r > 0.04045 { |
|||
r2 = math.Pow((r+0.055)/1.055, 2.4) |
|||
} else { |
|||
r2 = r / 12.92 |
|||
} |
|||
|
|||
if g > 0.04045 { |
|||
g2 = math.Pow((g+0.055)/1.055, 2.4) |
|||
} else { |
|||
g2 = g / 12.92 |
|||
} |
|||
|
|||
if b > 0.04045 { |
|||
b2 = math.Pow((b+0.055)/1.055, 2.4) |
|||
} else { |
|||
b2 = b / 12.92 |
|||
} |
|||
|
|||
return |
|||
} |
|||
|
|||
// reverseGamma applies a reverse gamma correction to the RGB values to make them
|
|||
func reverseGamma(r, g, b float64) (r2, g2, b2 float64) { |
|||
if r >= 0.0031308 { |
|||
r2 = 1.055*math.Pow(r, 1/2.4) - 0.055 |
|||
} else { |
|||
r2 = r * 12.92 |
|||
} |
|||
|
|||
if g >= 0.0031308 { |
|||
g2 = 1.055*math.Pow(g, 1/2.4) - 0.055 |
|||
} else { |
|||
g2 = g * 12.92 |
|||
} |
|||
|
|||
if b >= 0.0031308 { |
|||
b2 = 1.055*math.Pow(b, 1/2.4) - 0.055 |
|||
} else { |
|||
b2 = b * 12.92 |
|||
} |
|||
|
|||
return |
|||
} |
@ -0,0 +1,16 @@ |
|||
package huecolor_test |
|||
|
|||
import ( |
|||
"testing" |
|||
|
|||
"git.aiterp.net/lucifer/lucifer/internal/huecolor" |
|||
) |
|||
|
|||
func BenchmarkConvert(b *testing.B) { |
|||
for n := 0; n < b.N; n++ { |
|||
x, y := huecolor.ConvertXY(0.96, 0.04, 0.07) |
|||
if x < 0 && y < 0 { |
|||
b.Fail() |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,86 @@ |
|||
package huecolor |
|||
|
|||
// NOT IN USE. Gamuts just made things worse.
|
|||
|
|||
import "math" |
|||
|
|||
type gamutPoint [2]float64 |
|||
|
|||
// A ColorGamut is a subset of a color space, and is used to constrain colors.
|
|||
type ColorGamut struct { |
|||
R gamutPoint |
|||
G gamutPoint |
|||
B gamutPoint |
|||
} |
|||
|
|||
var ( |
|||
// HueGamut is the recommended gamut for hue bulbs
|
|||
HueGamut = ColorGamut{gamutPoint{0.675, 0.322}, gamutPoint{0.4091, 0.518}, gamutPoint{0.167, 0.04}} |
|||
|
|||
// FancyGamut is for LivingColors, Iris, and other fancy bulbs
|
|||
FancyGamut = ColorGamut{gamutPoint{0.704, 0.296}, gamutPoint{0.2151, 0.7106}, gamutPoint{0.138, 0.08}} |
|||
|
|||
// DefaultGamut is for non-Hue stuff.
|
|||
DefaultGamut = ColorGamut{gamutPoint{0.9961, 0.0001}, gamutPoint{0.0001, 0.9961}, gamutPoint{0.0001, 0.0001}} |
|||
) |
|||
|
|||
// Closest gets the closest point on the color gamut's edges. This function will always return a point
|
|||
// on an edge, even if `(x, y)` is inside of it.
|
|||
func (g *ColorGamut) Closest(x, y float64) (x2, y2 float64) { |
|||
p := gamutPoint{x, y} |
|||
pRG := closestToLine(g.R, g.G, p) |
|||
pGB := closestToLine(g.G, g.B, p) |
|||
pBR := closestToLine(g.B, g.R, p) |
|||
|
|||
dRG := distance(p, pRG) |
|||
dGB := distance(p, pGB) |
|||
dBR := distance(p, pBR) |
|||
|
|||
closest := pRG |
|||
closestDistance := dRG |
|||
if dGB < closestDistance { |
|||
closest = pGB |
|||
closestDistance = dGB |
|||
} |
|||
if dBR < closestDistance { |
|||
closest = pBR |
|||
closestDistance = dBR |
|||
} |
|||
|
|||
return closest[0], closest[1] |
|||
} |
|||
|
|||
// Contains retunrs true if the gamut contains the color.
|
|||
func (g *ColorGamut) Contains(x, y float64) bool { |
|||
pt := gamutPoint{x, y} |
|||
|
|||
b1 := sign(pt, g.R, g.G) < 0 |
|||
b2 := sign(pt, g.G, g.B) < 0 |
|||
b3 := sign(pt, g.B, g.R) < 0 |
|||
|
|||
return ((b1 == b2) && (b2 == b3)) |
|||
} |
|||
|
|||
func distance(p1, p2 gamutPoint) float64 { |
|||
dx := p1[0] - p2[0] |
|||
dy := p1[1] - p2[1] |
|||
return math.Sqrt(dx*dx + dy*dy) |
|||
} |
|||
|
|||
func closestToLine(a, b, p gamutPoint) gamutPoint { |
|||
ap := gamutPoint{p[0] - a[0], p[1] - a[1]} |
|||
ab := gamutPoint{b[0] - a[0], b[1] - a[1]} |
|||
|
|||
t := (ap[0]*ab[0] + ap[1]*ab[1]) / (ab[0]*ab[0] + ap[0]*ap[0]) |
|||
if t < 0 { |
|||
t = 0 |
|||
} else if t > 1 { |
|||
t = 1 |
|||
} |
|||
|
|||
return gamutPoint{a[0] + t*ab[0], a[1] + t*ab[1]} |
|||
} |
|||
|
|||
func sign(p1, p2, p3 gamutPoint) float64 { |
|||
return (p1[0]-p3[0])*(p2[1]-p3[1]) - (p2[0]-p3[0])*(p1[1]-p3[1]) |
|||
} |
@ -0,0 +1,17 @@ |
|||
package huecolor_test |
|||
|
|||
import ( |
|||
"testing" |
|||
|
|||
"git.aiterp.net/lucifer/lucifer/internal/huecolor" |
|||
) |
|||
|
|||
func TestGamut(t *testing.T) { |
|||
if !huecolor.DefaultGamut.Contains(0.1, 0.1) { |
|||
t.Error(1) |
|||
} |
|||
|
|||
if huecolor.DefaultGamut.Contains(0, 0) { |
|||
t.Error(2) |
|||
} |
|||
} |
@ -0,0 +1,35 @@ |
|||
package models |
|||
|
|||
import "context" |
|||
|
|||
// A Group is a collection of lights that relates to one another. The API should
|
|||
// allow all lights within it.
|
|||
type Group struct { |
|||
ID int `json:"id" db:"id"` |
|||
Name string `json:"name" db:"name"` |
|||
Permissions []GroupPermission `json:"permissions" db:"-"` |
|||
} |
|||
|
|||
// A GroupPermission is a permission for a user in a group.
|
|||
type GroupPermission struct { |
|||
GroupID int `json:"group_id" db:"group_id"` |
|||
UserID int `json:"user_id" db:"user_id"` |
|||
Read bool `json:"read" db:"read"` |
|||
Write bool `json:"write" db:"write"` |
|||
Create bool `json:"create" db:"create"` |
|||
Delete bool `json:"delete" db:"delete"` |
|||
Manage bool `json:"manage" db:"manage"` |
|||
} |
|||
|
|||
// GroupRepository is an interface for all database operations
|
|||
// the Group model makes.
|
|||
type GroupRepository interface { |
|||
FindByID(ctx context.Context, id int) (Group, error) |
|||
FindByLight(ctx context.Context, light Light) (Group, error) |
|||
List(ctx context.Context) ([]Group, error) |
|||
ListByUser(ctx context.Context, user User) ([]Group, error) |
|||
UpdatePermissions(ctx context.Context, permission GroupPermission) error |
|||
Insert(ctx context.Context, group Group) (Group, error) |
|||
Update(ctx context.Context, group Group) error |
|||
Remove(ctx context.Context, group Group) error |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue