display-test/api/pages/statuspage.go
2024-10-06 20:20:22 +03:00

300 lines
6.9 KiB
Go

package pages
import (
"context"
"fmt"
"math"
"sync"
"time"
"gitea.unprism.ru/KRBL/mpu/mpu"
"gitea.unprism.ru/KRBL/sim-modem/api/modem/gps"
"gitea.unprism.ru/yotia/display-test/components"
"gitea.unprism.ru/yotia/display-test/drawer"
)
// Debug variant:
// line0 - time
// time1 - tempreture, speed
// time2 - latitude
// time3 - longitude
const (
timeLayout = "01.09.06 15:04:05"
)
// Some layout constants
type statusPage struct {
drawer drawer.Drawer // Drawer with dysplay
// Status data
st systemStatus
timeShift time.Duration
// Visual components
line0 components.Text
line1 components.Text
line2 components.Text
line3 components.Text
// Layout values
signalX int
serviceX int
// Threads sync
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
}
// Only functions that control content of page
type StatusPageContent interface {
SystemStatusSetter
SetTimeShift(shift time.Duration)
}
type StatusPage interface {
Page
StatusPageContent
}
func NewStatusPage(d drawer.Drawer) (StatusPage, error) {
// Check display
if err := d.GetDisplay().IsReady(); err != nil {
return nil, fmt.Errorf("display is ready: %w", err)
}
// Create visual components
line0, err := components.NewText(d, 0, drawer.LineH*0)
if err != nil {
return nil, fmt.Errorf("create line0: %w", err)
}
line1, err := components.NewText(d, 0, drawer.LineH*1)
if err != nil {
return nil, fmt.Errorf("create line1: %w", err)
}
line2, err := components.NewText(d, 0, drawer.LineH*2)
if err != nil {
return nil, fmt.Errorf("create line2: %w", err)
}
line3, err := components.NewText(d, 0, drawer.LineH*3)
if err != nil {
return nil, fmt.Errorf("create line3: %w", err)
}
// Calculate signal and service glyphs' pos
signalX := d.W() - drawer.CommonGlyphs[drawer.SignalStatusGlyphI].W - drawer.CommonGlyphs[drawer.ServiceStatusGlyphI].W - drawer.CharGap
serviceX := d.W() - drawer.CommonGlyphs[drawer.ServiceStatusGlyphI].W
return &statusPage{
drawer: d,
line0: line0,
line1: line1,
line2: line2,
line3: line3,
signalX: signalX,
serviceX: serviceX,
}, nil
}
func (p *statusPage) Activate() {
// Draw
p.drawer.Clear()
//p.line0.SetStr("DEBUG line0")
//p.line1.SetStr("DEBUG line1")
//p.line2.SetStr("DEBUG line2")
//p.line3.SetStr("DEBUG line3")
p.line0.Draw()
p.line1.Draw()
p.line2.Draw()
p.line3.Draw()
// At the right-top corner there are signal then service status glyphs
// Layout:
// gap | signal | gap | service
p.SetRssi(0)
// Setup threads
p.ctx, p.cancel = context.WithCancel(context.Background())
go p.timeUpdateLoop()
}
func (p *statusPage) Diactivate() {
// Stop all support threads
p.cancel()
p.wg.Wait()
}
func (p *statusPage) SetTimeShift(shift time.Duration) {
p.st.mutex.Lock()
defer p.st.mutex.Unlock()
p.timeShift = shift
}
func (p *statusPage) SetGps(newData gps.Data) {
p.st.mutex.Lock()
defer p.st.mutex.Unlock()
p.st.gpsData = newData
// Format and update coords
// Langitude, longitude store format: ddmm.mmmmmm, dddmm.mmmmmm
// Latitude and longitude output format:
// DD° MM.MMM' N
// DDD° MM.MMM' W
latStr := fmt.Sprintf(" %02d°%06.3f' %s", int(p.st.gpsData.Latitude)/100, math.Mod(p.st.gpsData.Latitude, 100), p.st.gpsData.LatitudeIndicator)
p.line2.SetStr(latStr)
logStr := fmt.Sprintf(" %03d°%06.3f' %s", int(p.st.gpsData.Longitude)/100, math.Mod(p.st.gpsData.Longitude, 100), p.st.gpsData.LongitudeIndicator)
p.line3.SetStr(logStr)
}
func (p *statusPage) SetMpu(newData mpu.Data) {
p.st.mutex.Lock()
defer p.st.mutex.Unlock()
p.st.mpuData = newData
}
// Only safely updates local values
func (p *statusPage) setRssi(newData int) {
p.st.mutex.Lock()
defer p.st.mutex.Unlock()
p.st.rssi = newData
}
func (p *statusPage) setService(svc string) {
p.st.mutex.Lock()
defer p.st.mutex.Unlock()
p.st.service = svc
}
func getSignalStrength(rssi int) int {
// Signal strength clasification (from google):
// 0 - no signal -110 to ...
// 1 - poor signal -90 to -110
// 2 - fair signal -70 to -90
// 3 - good signal -50 to -70
// 4 - excelent signal ... to -50
// Description from "sim-modem/api/modem/utils/signal.go"
// 0 -113 dBm or less
// 1 -111 dBm
// 2...30 -109... -53 dBm
// 31 -51 dBm or greater
// 99 not known or not detectable
// 100 -116 dBm or less
// 101 -115 dBm
// 102…191 -114... -26dBm
// 191 -25 dBm or greater
// 199 not known or not detectable
// 100…199 expand to TDSCDMA, indicate RSCPreceived
// Cut certain cases
switch rssi {
case 99, 199: // not known or not detectable
return 0
case 0, 1, 100, 101: // no signal
return 0
case 31, 191: // Excelent
return 4
}
// Ranged values
if rssi >= 2 && rssi <= 30 {
// it is in -109...-53 dBm
// Simplified interpolation from 2..30 to -109...-53 and then to 0...4
return int((float64(rssi)*2-3)/20) + 1
}
if rssi >= 102 && rssi <= 191 {
// it is in -114...-26 dBm
// Simplified interpolation from 2..30 to -114...-26 and then to 0...4
return min(int((float64(rssi-102)+15)/20), 4)
}
return 0 // Invalid value
}
func getServiceStrength(service string) int {
// Service clasification by speed:
// 0 - no service - "NO SERVICE"
// 1 - 1G - do not use
// 2 - 2G - "GSM"
// 3 - 3G - "WCDMA"
// 4 - 4G - "LTE", "TDS"
switch service {
case "NO SERVICE":
return 0
case "GSM":
return 2
case "WCDMA":
return 3
case "LTE", "TDS":
return 4
}
return 0 // Invalid value
}
func (p *statusPage) SetRssi(rssi int) {
p.setRssi(rssi) // Save data (with short lock)
// Update screen image
ss := getSignalStrength(rssi) // Signal strength in [0; 4] range
p.drawer.CopyImg(p.signalX, 0, drawer.CommonGlyphs[drawer.SignalStatusGlyphI+ss])
p.drawer.GetDisplay().FlushByMask(p.drawer.GetDisplay().GetFlushMaskBit(1, 0))
}
func (p *statusPage) SetService(svc string) {
p.setService(svc) // Save data (with short lock)
// Update screen image
ss := getServiceStrength(svc) // Service strength in [0; 4] range
p.drawer.CopyImg(p.serviceX, 0, drawer.CommonGlyphs[drawer.ServiceStatusGlyphI+ss])
p.drawer.GetDisplay().FlushByMask(p.drawer.GetDisplay().GetFlushMaskBit(1, 0))
}
func (p *statusPage) timeUpdateLoop() {
p.wg.Add(1)
// Because ticker do not send signal immediately
p.line0.SetStr(time.Now().Add(p.timeShift).Format(timeLayout))
ticker := time.NewTicker(time.Second)
for {
select {
case <-p.ctx.Done():
ticker.Stop()
p.wg.Done()
return
case now := <-ticker.C:
p.line0.SetStr(now.Add(p.timeShift).Format(timeLayout))
}
}
}
func (p *statusPage) Close() (outErr error) {
// TODO Not the best way...
if err := p.line0.Close(); err != nil {
outErr = fmt.Errorf("line 0 close: %w:", err)
}
if err := p.line1.Close(); err != nil {
outErr = fmt.Errorf("line 1 close: %w:", err)
}
if err := p.line2.Close(); err != nil {
outErr = fmt.Errorf("line 2 close: %w:", err)
}
if err := p.line3.Close(); err != nil {
outErr = fmt.Errorf("line 3 close: %w:", err)
}
return
}