~emersion/drmdb

a9bed291adfea7d9bfa584c07b0557a48170363e — Simon Ser 2 years ago 5822da6
Add device page
6 files changed, 362 insertions(+), 18 deletions(-)

M db.go
M drmtree/drmtree.go
M public/assets/style.css
A public/device.html
M server.go
A treefmt/memory.go
M db.go => db.go +32 -16
@@ 15,6 15,8 @@ import (

const dbDir = "db"

var errStop = fmt.Errorf("drmdb: stop walking")

func generateKey(n *drmtree.Node) (string, error) {
	if n.Driver == nil || n.Device == nil {
		return "", fmt.Errorf("node is missing driver/device")


@@ 53,27 55,47 @@ func generateKey(n *drmtree.Node) (string, error) {
	return hex.EncodeToString(sum[:])[:12], nil
}

func store(n *drmtree.Node) error {
func store(n *drmtree.Node) (string, error) {
	k, err := generateKey(n)
	if err != nil {
		return err
		return "", err
	}

	p := filepath.Join(dbDir, k+".json")
	f, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
	if err != nil {
		if os.IsExist(err) {
			return fmt.Errorf("data has already been submitted")
			return "", fmt.Errorf("data has already been submitted")
		}
		return err
		return "", err
	}
	defer f.Close()

	if err := json.NewEncoder(f).Encode(n); err != nil {
		return err
		return "", err
	}

	return k, f.Close()
}

func _load(filename string) (*drmtree.Node, error) {
	p := filepath.Join(dbDir, filename)
	f, err := os.Open(p)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	var n drmtree.Node
	if err := json.NewDecoder(f).Decode(&n); err != nil {
		return nil, err
	}

	return &n, f.Close()
}

	return f.Close()
func load(k string) (*drmtree.Node, error) {
	return _load(k + ".json")
}

func walk(fn func(n *drmtree.Node) error) error {


@@ 83,20 105,14 @@ func walk(fn func(n *drmtree.Node) error) error {
	}

	for _, fi := range files {
		p := filepath.Join(dbDir, fi.Name())
		f, err := os.Open(p)
		if err != nil {
			return err
		}

		var n drmtree.Node
		err = json.NewDecoder(f).Decode(&n)
		f.Close()
		n, err := _load(fi.Name())
		if err != nil {
			return err
		}

		if err := fn(&n); err != nil {
		if err := fn(n); err == errStop {
			return nil
		} else if err != nil {
			return err
		}
	}

M drmtree/drmtree.go => drmtree/drmtree.go +198 -0
@@ 4,8 4,10 @@ package drmtree
import (
	"encoding/json"
	"fmt"
	"strings"

	"git.sr.ht/~emersion/go-drm"
	"git.sr.ht/~emersion/drmdb/treefmt"
)

type DriverVersion struct {


@@ 29,6 31,17 @@ type Kernel struct {
	Version string `json:"version"`
}

func (k *Kernel) String() string {
	if k.SysName == "" {
		return "unknown kernel"
	}
	s := k.SysName
	if k.Release != "" {
		s += " " + k.Release
	}
	return s
}

type Driver struct {
	Name       string             `json:"name"`
	Desc       string             `json:"desc"`


@@ 38,6 51,34 @@ type Driver struct {
	ClientCaps map[string]bool    `json:"client_caps"`
}

func (drv *Driver) FormatTree(tf treefmt.Formatter) {
	tf.Printf("Driver: %v (%v) version %v on %v", drv.Name, drv.Desc, &drv.Version, &drv.Kernel)
	tfc := tf.NewChild()

	for c, v := range drv.Caps {
		if v != nil {
			if c == "PRIME" {
				tfc.Printf("DRM_CAP_PRIME supported")
				tfcc := tfc.NewChild()
				tfcc.Printf("DRM_CAP_PRIME_IMPORT = %v", *v&drm.CapPrimeImport != 0)
				tfcc.Printf("DRM_CAP_PRIME_EXPORT = %v", *v&drm.CapPrimeExport != 0)
			} else {
				tfc.Printf("DRM_CAP_%v = %v", c, *v)
			}
		} else {
			tfc.Printf("DRM_CAP_%v unsupported", c)
		}
	}

	for c, ok := range drv.ClientCaps {
		if ok {
			tfc.Printf("DRM_CLIENT_CAP_%v supported", c)
		} else {
			tfc.Printf("DRM_CLIENT_CAP_%v unsupported", c)
		}
	}
}

type DevicePCI struct {
	Vendor    uint32 `json:"vendor"`
	Device    uint32 `json:"device"`


@@ 84,6 125,21 @@ func (dev *Device) UnmarshalJSON(b []byte) error {
	return nil
}

func (dev *Device) BusID() string {
	switch dev := dev.DeviceData.(type) {
	case *DevicePCI:
		return fmt.Sprintf("%04X:%04X", dev.Vendor, dev.Device)
	case *DevicePlatform:
		return strings.Join(dev.Compatible, "+")
	default:
		return ""
	}
}

func (dev *Device) FormatTree(tf treefmt.Formatter) {
	tf.Printf("Device: %v %v", dev.BusType, dev.BusID())
}

type Mode struct {
	Clock      uint32 `json:"clock"`
	HDisplay   uint16 `json:"hdisplay"`


@@ 265,6 321,18 @@ func (m *PropertyMap) UnmarshalJSON(b []byte) error {
	return nil
}

func (m *PropertyMap) FormatTree(tf treefmt.Formatter) {
	for name, prop := range *m {
		// TODO: immutable, atomic
		// TODO: type-specific property data
		val := prop.Data
		if val == nil {
			val = prop.Value
		}
		tf.Printf("%q: %v = %v", name, prop.Type, val)
	}
}

type Connector struct {
	ID         drm.ConnectorID     `json:"id"`
	Type       drm.ConnectorType   `json:"type"`


@@ 277,6 345,40 @@ type Connector struct {
	Properties PropertyMap         `json:"properties"`
}

func encoderIDsString(encs []drm.EncoderID) string {
	s := "{"
	for i, id := range encs {
		if i != 0 {
			s += ", "
		}
		s += fmt.Sprintf("%v", id)
	}
	s += "}"
	return s
}

func (conn *Connector) FormatTree(tf treefmt.Formatter) {
	tf.Printf("Object ID: %v", conn.ID)
	tf.Printf("Type: %v", conn.Type)
	tf.Printf("Status: %v", conn.Status)
	if conn.Status == drm.ConnectorStatusConnected {
		tf.Printf("Physical size: %vx%v mm", conn.PhyWidth, conn.PhyHeight)
		tf.Printf("Subpixel: %v", conn.Subpixel)
	}
	tf.Printf("Encoders: %v", encoderIDsString(conn.Encoders))
	if len(conn.Modes) > 0 {
		tf.Printf("Modes")
		tfc := tf.NewChild()
		for _, m := range conn.Modes {
			tfc.Printf("%v", &m)
		}
	}
	if len(conn.Properties) > 0 {
		tf.Printf("Properties")
		conn.Properties.FormatTree(tf.NewChild())
	}
}

type Encoder struct {
	ID             drm.EncoderID   `json:"id"`
	Type           drm.EncoderType `json:"type"`


@@ 285,6 387,29 @@ type Encoder struct {
	PossibleClones uint32          `json:"possible_clones"`
}

func bitfieldString(v uint32) string {
	s := "{"
	first := true
	for i := 0; i < 32; i++ {
		if v&(1<<uint(i)) != 0 {
			if !first {
				s += ", "
			}
			s += fmt.Sprintf("%v", i)
			first = false
		}
	}
	s += "}"
	return s
}

func (enc *Encoder) FormatTree(tf treefmt.Formatter) {
	tf.Printf("Object ID: %v", enc.ID)
	tf.Printf("Type: %v", enc.Type)
	tf.Printf("CRTCs: %v", bitfieldString(enc.PossibleCRTCs))
	tf.Printf("Clones: %v", bitfieldString(enc.PossibleClones))
}

type CRTC struct {
	ID         drm.CRTCID  `json:"id"`
	FB         drm.FBID    `json:"fb"`


@@ 295,6 420,20 @@ type CRTC struct {
	Properties PropertyMap `json:"properties"`
}

func (crtc *CRTC) FormatTree(tf treefmt.Formatter) {
	tf.Printf("Object ID: %v", crtc.ID)
	tf.Printf("FB: %v", crtc.FB)
	tf.Printf("Position: %v, %v", crtc.X, crtc.Y)
	tf.Printf("Gamma size: %v", crtc.GammaSize)
	if crtc.Mode != nil {
		tf.Printf("Mode: %v", crtc.Mode)
	}
	if len(crtc.Properties) > 0 {
		tf.Printf("Properties")
		crtc.Properties.FormatTree(tf.NewChild())
	}
}

type Plane struct {
	ID            drm.PlaneID  `json:"id"`
	CRTC          drm.CRTCID   `json:"crtc"`


@@ 305,6 444,25 @@ type Plane struct {
	Properties    PropertyMap  `json:"properties"`
}

func (plane *Plane) FormatTree(tf treefmt.Formatter) {
	tf.Printf("Object ID: %v", plane.ID)
	tf.Printf("CRTC: %v", plane.CRTC)
	tf.Printf("FB: %v", plane.FB)
	tf.Printf("CRTCs: %v", bitfieldString(plane.PossibleCRTCs))
	tf.Printf("Gamma size: %v", plane.GammaSize)
	if len(plane.Formats) > 0 {
		tf.Printf("Formats")
		tfc := tf.NewChild()
		for _, fmt := range plane.Formats {
			tfc.Printf("%v", fmt)
		}
	}
	if len(plane.Properties) > 0 {
		tf.Printf("Properties")
		plane.Properties.FormatTree(tf.NewChild())
	}
}

type Node struct {
	Driver     *Driver     `json:"driver"`
	Device     *Device     `json:"device"`


@@ 314,4 472,44 @@ type Node struct {
	Planes     []Plane     `json:"planes"`
}

func (n *Node) FormatTree(tf treefmt.Formatter) {
	n.Device.FormatTree(tf)
	n.Driver.FormatTree(tf)

	tf.Printf("Connectors")
	for i, conn := range n.Connectors {
		tfc := tf.NewChild()
		tfc.Printf("Connector %v", i)
		conn.FormatTree(tfc.NewChild())
	}

	tf.Printf("Encoders")
	for i, enc := range n.Encoders {
		tfc := tf.NewChild()
		tfc.Printf("Encoder %v", i)
		enc.FormatTree(tfc.NewChild())
	}

	tf.Printf("CRTCs")
	for i, crtc := range n.CRTCs {
		tfc := tf.NewChild()
		tfc.Printf("CRTC %v", i)
		crtc.FormatTree(tfc.NewChild())
	}

	tf.Printf("Planes")
	for i, plane := range n.Planes {
		tfc := tf.NewChild()
		tfc.Printf("Plane %v", i)
		plane.FormatTree(tfc.NewChild())
	}
}

type NodeMap map[string]*Node

func (m NodeMap) FormatTree(tf treefmt.Formatter) {
	for path, n := range m {
		tf.Printf("Node: %s", path)
		n.FormatTree(tf.NewChild())
	}
}

M public/assets/style.css => public/assets/style.css +36 -0
@@ 56,3 56,39 @@ td.status-unknown {
td.pre {
    font-family: monospace;
}

ul.pre {
    font-family: monospace;
}

ul.pre, ul.pre ul {
    list-style: none;
    padding: 0;
}
ul.pre ul {
    margin: 0;
    margin-left: 1.0em;
}

ul.pre ul li {
    margin-left: 0.35em;
    border-left: thin solid #444;
}

ul.pre ul li:last-child {
    border-left: none;
}

ul.pre ul li:before {
    width: 0.9em;
    height: 0.6em;
    margin-right: 0.1em;
    vertical-align: top;
    border-bottom: thin solid #444;
    content: "";
    display: inline-block;
}

ul.pre ul li:last-child:before {
    border-left: thin solid #444;
}

A public/device.html => public/device.html +26 -0
@@ 0,0 1,26 @@
{{template "head" "Device"}}

<h1>DRM database device</h1>

<p>
	<a href="/">Back to index</a><br>
	<a href="/devices/{{.Key}}.json">.json</a>
</p>

{{define "tree"}}
	<li>{{.Text}}
		{{if gt (len .Children) 0}}
		<ul>
			{{range .Children}}
			{{template "tree" .}}
			{{end}}
		</ul>
		{{end}}
	</li>
{{end}}

<ul class="pre">
	{{range .Tree}}
	{{template "tree" .}}
	{{end}}
</ul>

M server.go => server.go +34 -2
@@ 10,6 10,7 @@ import (
	"strings"

	"git.sr.ht/~emersion/drmdb/drmtree"
	"git.sr.ht/~emersion/drmdb/treefmt"
	"git.sr.ht/~emersion/go-drm"
	"git.sr.ht/~emersion/go-hwids"
	"github.com/labstack/echo/v4"


@@ 171,10 172,11 @@ func New() *echo.Echo {

		success := false
		for name, n := range nodes {
			if err := store(n); err != nil {
			if key, err := store(n); err != nil {
				fmt.Fprintf(c.Response(), "%s: error: %v\n", name, err)
			} else {
				fmt.Fprintf(c.Response(), "%s: data uploaded\n", name)
				u := "https://" + c.Request().Host + "/devices/" + key
				fmt.Fprintf(c.Response(), "%s: data uploaded to %s\n", name, u)
				success = true
			}
		}


@@ 240,6 242,36 @@ func New() *echo.Echo {
		}{devices})
	})

	e.GET("/devices/:key", func(c echo.Context) error {
		key := c.Param("key")
		raw := false
		if strings.HasSuffix(key, ".json") {
			key = strings.TrimSuffix(key, ".json")
			raw = true
		}

		n, err := load(key)
		if err != nil {
			if os.IsNotExist(err) {
				return c.String(http.StatusNotFound, "no such device")
			}
			return err
		}

		if raw {
			return c.JSON(http.StatusOK, n)
		} else {
			tf := treefmt.NewMemoryFormatter()
			n.FormatTree(tf)

			return c.Render(http.StatusOK, "device.html", struct{
				Key string
				Node *drmtree.Node
				Tree []treefmt.Memory
			}{key, n, tf.Tree()})
		}
	})

	e.GET("/capabilities", func(c echo.Context) error {
		var drivers []string
		caps := make(map[string]map[string]*uint64)

A treefmt/memory.go => treefmt/memory.go +36 -0
@@ 0,0 1,36 @@
package treefmt

import (
	"fmt"
)

type Memory struct {
	Text string
	Children []Memory
}

type MemoryFormatter struct {
	m *Memory
}

// NewMemoryFormatter pretty-prints a tree in an in-memory data structure.
func NewMemoryFormatter() *MemoryFormatter {
	return &MemoryFormatter{m: &Memory{}}
}

func (mf *MemoryFormatter) NewChild() Formatter {
	if len(mf.m.Children) == 0 {
		panic("treefmt: NewChild called before Printf")
	}
	m := &mf.m.Children[len(mf.m.Children) - 1]
	return &MemoryFormatter{m: m}
}

func (mf *MemoryFormatter) Printf(format string, v ...interface{}) {
	t := fmt.Sprintf(format, v...)
	mf.m.Children = append(mf.m.Children, Memory{Text: t})
}

func (mf *MemoryFormatter) Tree() []Memory {
	return mf.m.Children
}