diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8d357b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +go-rpi-rgb-led-matrix/lib/rpi-rgb-led-matrix/ diff --git a/cmd/rpi/main.go b/cmd/rpi/main.go index d25bd2e..c4ec47b 100644 --- a/cmd/rpi/main.go +++ b/cmd/rpi/main.go @@ -5,6 +5,8 @@ import ( "fmt" "image/color" "log" + "net/http" + "sync" "time" "git.dvdt.dev/david/bitcoin-ticker-pi/display" @@ -14,8 +16,8 @@ import ( ) const ( - RefreshRateHz = 60 - MempoolURL = "wss://mempool.dvdt.dev/v1/api/ws" + RefreshRateHz = 1 + MempoolURL = "mempool.space" ) func main() { @@ -31,38 +33,92 @@ func main() { display.NewSlot(5, 5, 4, 44, 0), display.NewSlot(7, 13, 8, 5, 9), display.NewSlot(12, 5, 4, 7, 27), - display.NewSlot(8, 5, 4, 28, 27), ) ctx, cancel := context.WithCancel(context.Background()) + defer cancel() ticker := time.NewTicker(time.Second / RefreshRateHz) tdone := make(chan struct{}) mdone := make(chan struct{}) - blocks := make(chan mempool.Block) + msgs := make(chan mempool.Message) errs := make(chan error) - mp := mempool.New(MempoolURL) + mp := mempool.New(MempoolURL, &http.Client{ + Timeout: 5 * time.Second, + }) err := mp.Init() if err != nil { log.Fatal(err) } - go func(done chan struct{}, t *time.Ticker) { + currentBlockHeight, err := mp.CurrentBlockHeight() + if err != nil { + log.Fatal(err) + } + + highfee, medfee, lowfee, err := mp.CurrentFees() + if err != nil { + log.Fatal(err) + } + + var blockHeight int + blockHeightMutex := &sync.Mutex{} + var fees mempool.Fees + feesMutex := &sync.Mutex{} + var price int + priceMutex := &sync.Mutex{} + + blockHeight = currentBlockHeight + fees.FastestFee = highfee + fees.HalfHourFee = medfee + fees.HourFee = lowfee + + go mp.Listen(ctx, msgs, errs, mdone) + + go func() { for { select { - case <-done: - return - case <-ticker.C: - updateSlots(d, 44000, 821000, 100, 200, 300) + case m := <-msgs: + fmt.Printf("%+v\n", m) + if m.Fees != nil { + feesMutex.Lock() + fees = *m.Fees + feesMutex.Unlock() + } + if m.Block != nil { + blockHeightMutex.Lock() + blockHeight = int(m.Block.Height) + blockHeightMutex.Unlock() + } + if m.Conversions != nil { + priceMutex.Lock() + price = m.Conversions.USD + priceMutex.Unlock() + } } } - }(tdone, ticker) + }() + + for { + select { + case <-tdone: + return + case <-ticker.C: + d.Clear() + d.Render() + err = updateSlots(d, price, blockHeight, fees.HourFee, fees.HalfHourFee, fees.FastestFee) + if err != nil { + log.Fatal(err) + } + } + } } func updateSlots(d *display.Display, price, height, lfee, mfee, hfee int) error { - priceStr := fmt.Sprintf("%d", price) + d.Clear() + var priceSegments []display.Segment - priceSegments = append(priceSegments, strToDisplaySegments("dollar", fonts.Green)...) + priceSegments = append(priceSegments, display.Segment{Text: "dollar", Color: fonts.Green}) priceSegments = append(priceSegments, strToDisplaySegments(fmt.Sprintf("%d", price), fonts.White)...) err := d.UpdateTopLeft(priceSegments) @@ -71,8 +127,12 @@ func updateSlots(d *display.Display, price, height, lfee, mfee, hfee int) error } var moscowTimeSegments []display.Segment - moscowTimeSegments = append(moscowTimeSegments, strToDisplaySegments("satoshi", fonts.Orange)...) - moscowTimeSegments = append(moscowTimeSegments, strToDisplaySegments(fmt.Sprintf("%d", 100000000/price), fonts.White)...) + moscowTime := 0 + if price > 0 { + moscowTime = 100000000 / price + } + moscowTimeSegments = append(moscowTimeSegments, display.Segment{Text: "satoshi", Color: fonts.Orange}) + moscowTimeSegments = append(moscowTimeSegments, strToDisplaySegments(fmt.Sprintf("%d", moscowTime), fonts.White)...) err = d.UpdateTopRight(moscowTimeSegments) if err != nil { @@ -80,18 +140,31 @@ func updateSlots(d *display.Display, price, height, lfee, mfee, hfee int) error } var heightSegments []display.Segment - heightSegments = append(heightSegments, strToDisplaySegments("height", fonts.Blue)...) + heightSegments = append(heightSegments, display.Segment{Text: "height", Color: fonts.Blue}) heightSegments = append(heightSegments, strToDisplaySegments(fmt.Sprintf("%d", height), fonts.White)...) - err = d.UpdateCenter(heightSegments) + err = d.UpdateMain(heightSegments) if err != nil { return err } - return nil + var feeSegments []display.Segment + feeSegments = append(feeSegments, display.Segment{Text: "low", Color: fonts.Green}) + feeSegments = append(feeSegments, strToDisplaySegments(fmt.Sprintf("%d", lfee), fonts.White)...) + feeSegments = append(feeSegments, display.Segment{Text: "medium", Color: fonts.Orange}) + feeSegments = append(feeSegments, strToDisplaySegments(fmt.Sprintf("%d", mfee), fonts.White)...) + feeSegments = append(feeSegments, display.Segment{Text: "high", Color: fonts.Red}) + feeSegments = append(feeSegments, strToDisplaySegments(fmt.Sprintf("%d", hfee), fonts.White)...) + + err = d.UpdateBottom(feeSegments) + if err != nil { + return err + } + + return d.Render() } -func strToDisplaySegments(s string, c color.Color) []display.Segment { +func strToDisplaySegments(s string, c color.RGBA) []display.Segment { var segs []display.Segment for _, r := range s { segs = append(segs, display.Segment{Text: string(r), Color: c}) diff --git a/display/display.go b/display/display.go index a90951c..96d607a 100644 --- a/display/display.go +++ b/display/display.go @@ -1,71 +1,69 @@ package display import ( + "image" + "image/draw" + + "git.dvdt.dev/david/bitcoin-ticker-pi/fonts" rgbmatrix "git.dvdt.dev/david/bitcoin-ticker-pi/go-rpi-rgb-led-matrix" ) type Display struct { canvas *rgbmatrix.Canvas - tl, tr, c, bl, br *Slot + topleft, topright, main, bottom *Slot } -func New(canvas *rgbmatrix.Canvas, tl, tr, c, bl, br *Slot) *Display { - return &display{ - canvas: canvas, - tl: tl, - tr: tr, - c: c, - bl: bl, - br: br, +func New(canvas *rgbmatrix.Canvas, topleft, topright, main, bottom *Slot) *Display { + return &Display{ + canvas: canvas, + topleft: topleft, + topright: topright, + main: main, + bottom: bottom, } } func (d *Display) UpdateTopLeft(text []Segment) error { - err := d.tl.Update(text) + err := d.topleft.Update(text) if err != nil { return err } - d.tl.Draw(d.canvas) + d.topleft.Draw(d.canvas) return nil } func (d *Display) UpdateTopRight(text []Segment) error { - err := d.tr.Update(text) + err := d.topright.Update(text) if err != nil { return err } - d.tr.Draw(d.canvas) + d.topright.Draw(d.canvas) return nil } -func (d *Display) UpdateCenter(text []Segment) error { - err := d.c.Update(text) +func (d *Display) UpdateMain(text []Segment) error { + err := d.main.Update(text) if err != nil { return err } - d.c.Draw(d.canvas) + d.main.Draw(d.canvas) return nil } -func (d *Display) UpdateBottomLeft(text []Segment) error { - err := d.bl.Update(text) +func (d *Display) UpdateBottom(text []Segment) error { + err := d.bottom.Update(text) if err != nil { return err } - d.bl.Draw(d.canvas) - return nil -} - -func (d *Display) UpdateBottomRight(text []Segment) error { - err := d.br.Update(text) - if err != nil { - return err - } - d.bl.Draw(d.canvas) + d.bottom.Draw(d.canvas) return nil } func (d *Display) Render() error { return d.canvas.Render() } + +func (d *Display) Clear() { + draw.Draw(d.canvas, d.canvas.Bounds(), &image.Uniform{fonts.Black}, image.ZP, draw.Src) +} diff --git a/fonts/13x8.go b/fonts/13x8.go index 6d02335..d66945f 100644 --- a/fonts/13x8.go +++ b/fonts/13x8.go @@ -21,7 +21,7 @@ var ( {0, 1, 1, 1, 1, 1, 0, 0}, {1, 1, 1, 0, 1, 1, 1, 0}, {1, 1, 0, 0, 0, 1, 1, 0}, - {1, 0, 0, 0, 1, 1, 1, 0}, + {0, 0, 0, 0, 1, 1, 1, 0}, {0, 0, 0, 0, 1, 1, 1, 0}, {0, 0, 0, 1, 1, 1, 0, 0}, {0, 0, 1, 1, 1, 0, 0, 0}, @@ -108,19 +108,19 @@ var ( {0, 0, 1, 1, 0, 0, 0, 0}, }, "8": { - {0, 0, 1, 1, 1, 0, 0, 0}, {0, 1, 1, 1, 1, 1, 0, 0}, - {1, 1, 1, 0, 1, 1, 1, 0}, + {1, 1, 1, 1, 1, 1, 1, 0}, + {1, 1, 0, 0, 0, 1, 1, 0}, {1, 1, 0, 0, 0, 1, 1, 0}, - {1, 1, 1, 0, 1, 1, 1, 0}, - {0, 1, 1, 1, 1, 1, 0, 0}, - {0, 0, 1, 1, 1, 0, 0, 0}, - {0, 1, 1, 1, 1, 1, 0, 0}, - {1, 1, 1, 0, 1, 1, 1, 0}, {1, 1, 0, 0, 0, 1, 1, 0}, - {1, 1, 1, 0, 1, 1, 1, 0}, {0, 1, 1, 1, 1, 1, 0, 0}, {0, 0, 1, 1, 1, 0, 0, 0}, + {0, 1, 1, 1, 1, 1, 0, 0}, + {1, 1, 0, 0, 0, 1, 1, 0}, + {1, 1, 0, 0, 0, 1, 1, 0}, + {1, 1, 0, 0, 0, 1, 1, 0}, + {1, 1, 1, 1, 1, 1, 1, 0}, + {0, 1, 1, 1, 1, 1, 0, 0}, }, "9": { {0, 0, 1, 1, 1, 0, 0, 0}, @@ -134,8 +134,8 @@ var ( {0, 0, 0, 0, 0, 1, 1, 0}, {0, 0, 0, 0, 0, 1, 1, 0}, {0, 1, 0, 0, 1, 1, 1, 0}, - {0, 1, 1, 0, 0, 0, 0, 0}, - {0, 0, 1, 1, 0, 0, 0, 0}, + {0, 1, 1, 1, 1, 1, 1, 0}, + {0, 0, 1, 1, 1, 1, 0, 0}, }, "0": { {0, 1, 1, 1, 1, 1, 0, 0}, diff --git a/mempool/mempool.go b/mempool/mempool.go index d2428b0..da89b93 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -5,7 +5,11 @@ import ( "encoding/json" "errors" "fmt" + "io" + "net/http" "net/url" + "strconv" + "time" "github.com/gorilla/websocket" ) @@ -14,19 +18,27 @@ var ( ErrNotWebsocket = errors.New("provided addr was not websocket") ) +const ( + WSEndpoint = "api/v1/ws" +) + type mempool struct { - addr string - ws *websocket.Conn + baseUrl string + httpClient *http.Client + ws *websocket.Conn } -func New(addr string) *mempool { +func New(baseUrl string, httpClient *http.Client) *mempool { return &mempool{ - addr: addr, + baseUrl: baseUrl, + httpClient: &http.Client{ + Timeout: 5 * time.Second, + }, } } func (m *mempool) Init() error { - u, err := url.Parse(m.addr) + u, err := url.Parse(fmt.Sprintf("wss://%s/%s", m.baseUrl, WSEndpoint)) if err != nil { return err } @@ -40,15 +52,93 @@ func (m *mempool) Init() error { return err } + b, err := json.Marshal(struct { + Action string `json:"action"` + Data []string `json:"data"` + }{ + Action: "want", + Data: []string{"blocks", "stats"}, + }) + if err != nil { + return err + } + + if err = c.WriteMessage(websocket.BinaryMessage, b); err != nil { + return err + } + m.ws = c return nil } -func (m *mempool) Listen(ctx context.Context, blocks chan Block, errs chan error, done chan struct{}) { +func (m *mempool) CurrentBlockHeight() (int, error) { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s/api/blocks/tip/height", m.baseUrl), nil) + if err != nil { + return 0, err + } + + resp, err := m.httpClient.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + if resp.StatusCode >= http.StatusInternalServerError { + return 0, errors.New("server error") + } + + if resp.StatusCode >= http.StatusBadRequest { + return 0, errors.New("client error") + } + + b, err := io.ReadAll(resp.Body) + if err != nil { + return 0, err + } + + return strconv.Atoi(string(b)) +} + +func (m *mempool) CurrentFees() (int, int, int, error) { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s/api/v1/fees/recommended", m.baseUrl), nil) + if err != nil { + return 0, 0, 0, err + } + + resp, err := m.httpClient.Do(req) + if err != nil { + return 0, 0, 0, err + } + defer resp.Body.Close() + + if resp.StatusCode >= http.StatusInternalServerError { + return 0, 0, 0, err + } + + if resp.StatusCode >= http.StatusBadRequest { + return 0, 0, 0, err + } + + b, err := io.ReadAll(resp.Body) + if err != nil { + return 0, 0, 0, err + } + + var data struct { + Fastest int `json:"fastestFee"` + HalfHour int `json:"halfHourFee"` + Hour int `json:"hourFee"` + } + + err = json.Unmarshal(b, &data) + return data.Fastest, data.HalfHour, data.Hour, err +} + +func (m *mempool) Listen(ctx context.Context, messages chan Message, errs chan error, done chan struct{}) { for { select { case <-ctx.Done(): - close(blocks) + close(messages) close(errs) close(done) return @@ -59,14 +149,14 @@ func (m *mempool) Listen(ctx context.Context, blocks chan Block, errs chan error continue } - var b Block - err = json.Unmarshal(message, &b) + var m Message + err = json.Unmarshal(message, &m) if err != nil { errs <- err continue } - blocks <- b + messages <- m } } } diff --git a/mempool/models.go b/mempool/models.go index 3582b37..e4568ae 100644 --- a/mempool/models.go +++ b/mempool/models.go @@ -1,5 +1,29 @@ package mempool +type Message struct { + Block *Block `json:"block"` + Conversions *Conversions `json:"conversions"` + Fees *Fees `json:"fees"` +} + type Block struct { Height int64 `json:"height"` } + +type Conversions struct { + USD int `json:"USD"` + EUR int `json:"EUR"` + GBP int `json:"GBP"` + CAD int `json:"CAD"` + CHF int `json:"CHF"` + AUD int `json:"AUD"` + JPY int `json:"JPY"` +} + +type Fees struct { + FastestFee int `json:"fastestFee"` + HalfHourFee int `json:"halfHourFee"` + HourFee int `json:"hourFee"` + EconomyFee int `json:"economyFee"` + MinimumFee int `json:"minimumFee"` +}