display-test/api/pages/statuspage.go

371 lines
8.5 KiB
Go
Raw Normal View History

2024-09-30 18:56:45 +00:00
package pages
import (
"context"
"fmt"
"math"
"sync"
2024-10-06 18:31:23 +00:00
"sync/atomic"
2024-09-30 18:56:45 +00:00
"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"
)
2024-10-06 18:31:23 +00:00
/*
Status page has status line at the top and three lines with primary data below.
Status line shows time and signal and service icon at the right corner.
Main space can show several screens:
1) GPS
GPS coordinates
2) System state
temepature
speed
may be amount of cameras
Other pages can show for ex important logs.
*/
2024-09-30 18:56:45 +00:00
const (
timeLayout = "01.09.06 15:04:05"
2024-10-06 18:31:23 +00:00
screenN = 2
screenChangeTimeout = 6 * time.Second
)
2024-09-30 18:56:45 +00:00
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
2024-10-06 18:31:23 +00:00
// Current screen(index) (default - 0)
curScreen atomic.Int32
2024-09-30 18:56:45 +00:00
// 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
2024-09-30 18:56:45 +00:00
return &statusPage{
drawer: d,
line0: line0,
line1: line1,
line2: line2,
line3: line3,
signalX: signalX,
serviceX: serviceX,
2024-09-30 18:56:45 +00:00
}, nil
}
func (p *statusPage) Activate() {
// Draw
p.drawer.Clear()
2024-10-06 18:31:23 +00:00
p.SetRssi(0)
2024-10-06 18:31:23 +00:00
p.SetService("NO SERVICE")
2024-09-30 18:56:45 +00:00
// Setup threads
p.ctx, p.cancel = context.WithCancel(context.Background())
go p.timeUpdateLoop()
2024-10-06 18:31:23 +00:00
go p.screenChangeLoop()
2024-09-30 18:56:45 +00:00
}
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()
p.st.gpsData = newData
2024-10-06 18:31:23 +00:00
p.st.mutex.Unlock()
2024-09-30 18:56:45 +00:00
2024-10-06 18:31:23 +00:00
if p.curScreen.Load() == 0 { // Draw only on first screen
p.drawCoords(&newData)
}
2024-09-30 18:56:45 +00:00
}
func (p *statusPage) SetMpu(newData mpu.Data) {
p.st.mutex.Lock()
p.st.mpuData = newData
2024-10-06 18:31:23 +00:00
p.st.mutex.Unlock()
if p.curScreen.Load() == 1 { // Draw only on second screen
p.drawMpu(&newData)
}
2024-09-30 18:56:45 +00:00
}
2024-10-06 18:31:23 +00:00
func (p *statusPage) SetRssi(rssi int) {
// Save data
2024-09-30 18:56:45 +00:00
p.st.mutex.Lock()
2024-10-06 18:31:23 +00:00
p.st.rssi = rssi
p.st.mutex.Unlock()
2024-09-30 18:56:45 +00:00
2024-10-06 18:31:23 +00:00
// Draw
p.drawRssi(rssi)
2024-09-30 18:56:45 +00:00
}
2024-10-06 18:31:23 +00:00
func (p *statusPage) SetService(svc string) {
// Save data
2024-09-30 18:56:45 +00:00
p.st.mutex.Lock()
p.st.service = svc
2024-10-06 18:31:23 +00:00
p.st.mutex.Unlock()
// Draw
p.drawService(svc)
}
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
}
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
}
2024-10-06 18:31:23 +00:00
// Update time in top line
2024-09-30 18:56:45 +00:00
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))
}
}
}
2024-10-06 18:31:23 +00:00
func (p *statusPage) screenChangeLoop() {
p.wg.Add(1)
2024-09-30 18:56:45 +00:00
2024-10-06 18:31:23 +00:00
// Because ticker do not send signal immediately
ticker := time.NewTicker(screenChangeTimeout)
for {
select {
case <-p.ctx.Done():
ticker.Stop()
p.wg.Done()
return
case <-ticker.C:
p.changeScreen()
}
2024-09-30 18:56:45 +00:00
}
2024-10-06 18:31:23 +00:00
}
func (p *statusPage) changeScreen() {
scr := p.curScreen.Load()
scr = (scr + 1) % screenN
p.curScreen.Store(scr)
p.drawScreen(int(scr))
}
///////////////////// Draw functions /////////////////////
// Draw functions get draw data as arguments because otherwise they have to get it using mutex
// Moreover they will lock it second time
func (p *statusPage) drawRssi(rssi int) {
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) drawService(svc string) {
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) drawCoords(data *gps.Data) {
// Langitude, longitude store format: ddmm.mmmmmm, dddmm.mmmmmm
// Latitude and longitude layout:
// 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) drawMpu(data *mpu.Data) {
// Layout:
// temp ttt.ttC
tempStr := fmt.Sprintf("temperature %06.3fC", data.TempData)
p.line2.SetStr(tempStr)
}
// Draw main part depends on what current screen is active
func (p *statusPage) drawScreen(screen int) {
// Clear
//p.drawer.FillBar(0, drawer.LineH, p.drawer.W(), p.drawer.H()-drawer.LineH, 0)
//p.drawer.GetDisplay().FlushByMask(0b01110111)
p.line1.SetStr("")
p.line2.SetStr("")
p.line3.SetStr("")
switch screen {
case 0:
p.st.mutex.Lock()
gpsData := &p.st.gpsData
p.st.mutex.Unlock()
p.drawCoords(gpsData)
case 1:
p.st.mutex.Lock()
mpuData := &p.st.mpuData
p.st.mutex.Unlock()
p.drawMpu(mpuData)
p.line1.SetStr("47 cameras connected")
p.line3.SetStr("some other info...")
2024-09-30 18:56:45 +00:00
}
}