package pages import ( "context" "fmt" "math" "sync" "sync/atomic" "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" ) /* 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. */ const ( timeLayout = "01.09.06 15:04:05" screenN = 2 screenChangeTimeout = 6 * time.Second ) 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 // Current screen(index) (default - 0) curScreen atomic.Int32 // 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.SetRssi(0) p.SetService("NO SERVICE") // Setup threads p.ctx, p.cancel = context.WithCancel(context.Background()) go p.timeUpdateLoop() go p.screenChangeLoop() } 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 p.st.mutex.Unlock() if p.curScreen.Load() == 0 { // Draw only on first screen p.drawCoords(&newData) } } func (p *statusPage) SetMpu(newData mpu.Data) { p.st.mutex.Lock() p.st.mpuData = newData p.st.mutex.Unlock() if p.curScreen.Load() == 1 { // Draw only on second screen p.drawMpu(&newData) } } func (p *statusPage) SetRssi(rssi int) { // Save data p.st.mutex.Lock() p.st.rssi = rssi p.st.mutex.Unlock() // Draw p.drawRssi(rssi) } func (p *statusPage) SetService(svc string) { // Save data p.st.mutex.Lock() p.st.service = svc 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 } // Update time in top line 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) screenChangeLoop() { p.wg.Add(1) // 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() } } } 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...") } }