Share My Creation dashboard

ic_launcher.png

  • Analog to digital signal conversion:
    • Engine speed signal for 1, 2, 3, or 4 cylinders.
    • Speed signal from a bridge inductive sensor or Hall sensor.
    • Settings for km/h or mph, wheel circumference, number of pulses per revolution.
  • Various indicators:
    • Neutral indicator.
    • Gear position indicator for gears 1 to 6.
    • Fuel reserve indicator.
    • Low battery indicator.
    • Turn signal indicator (right, left, hazard).
    • High beam indicator.
    • Position lights indicator.
    • ABS indicator.
    • Temperature indicator.
    • Oil pressure indicator.
    • Choke indicator.
    • Stop control indicator.
    • Fuel level gauge.
    • Multi-screen display (4 screens).
  • Relay controls:
    • On/off relay control.
    • Timed relay control.
googleplay.jpg


Attached is a Windows x64 and x86 application designed to simulate sending data from a PC to a smartphone. Configure your PC's Bluetooth LE to use port 1 to 5 and rename it to 'AEON'. You can then connect your phone to the 'AEON' SSID. Using the simulator you will be able to send data to your phone. If your computer does not have Bluetooth LE you must purchase a USB Bluetooth 4.0 HM-10 or BT-09 compatible adapter

DASHBOARD_SIMULATOR_x64
DASHBOARD_SIMULATOR_x86
Simplified B4A script for demonstrations

AEON DASHBOARD Android
AEON DASHBOARD iOS

Simplified B4A script for demonstrations:
'' This script was written to demonstrate the Bluetooth Low Energy connection with the
'' AEON electronic interface or with PC simulation software. It is intended to provide
'' useful information for the development of various graphics applications in line with
'' the AEON product.
'' Written by André FAËS (Mr Blue Sky)

#Region Shared Files
#CustomBuildAction: folders ready, %WINDIR%\System32\Robocopy.exe,"..\..\Shared Files" "..\Files"
'Ctrl + click to sync files: ide://run?file=%WINDIR%\System32\Robocopy.exe&args=..\..\Shared+Files&args=..\Files&FilesSync=True
#End Region

'Ctrl + click to export as zip: ide://run?File=%B4X%\Zipper.jar&Args=BLEExample.zip

' Class declaration and initialization of global variables
' defines variables used across the class, including UI elements
' like buttons and gauges, and managers for BLE services and execution permissions.
Sub Class_Globals
    Private Root As B4XView
    Private xui As XUI
    Private manager As BleManager2
    Private rp As RuntimePermissions
    Dim PO As PhoneOrientation
    Private pnlLayout As Panel
    Private btnDisconnect As B4XView
    Private btnScan As B4XView
    Private lblDeviceStatus As B4XView
    Private currentStateText As String = "UNKNOWN"
    Private currentState As Int
    Private connected As Boolean = False
    Private ConnectedName As String = "AEON"
    Private ConnectedServices As List
    Private pbScan As B4XLoadingIndicator
    Private xSpeedGauge As xGauges
    Private xFuelGauge As xGauges
    Private xTiltGauge As xGauges
    Private xRPMGauge As xGauges
    Private fntTANK, fntOIL, fntTEMP, fntBATT, fntCHOKE, fntBREAK, fntABS, fntDEFAULT, fntLEFT, fntRIGHT, fntLOWBEAM, fntHIGHBEAM As B4XView
    Private lblGear, lblODO, lblTRIP As Label
    Private bt_trip As B4XView
    Private dialog As B4XDialog
    Private speedview As SnakeView
    Dim bmpSpeedview As BitmapDrawable
End Sub

' Method provided for initializing additional components or databases if necessary, currently commented
Public Sub Initialize
'    If dBSQL.IsInitialized = False Then
'        InitdbSQL
'    End If
End Sub

' Load the main layout, initialize the dialog, BLE manager, configure gauges,
' start listening for device orientation, and configure policies and colors.
' Each of these methods initializes a specific part of the application,
' such as panel settings, dialogs, handlers, gauges, etc.
Private Sub B4XPage_Created (Root1 As B4XView)
    Root = Root1
    Root.LoadLayout("main")
    InitializePanelSettings
    InitializeDialog
    InitializeManager
    ConfigureGauges
    StartDeviceOrientationListening
    ConfigureFontsAndColors
End Sub

' Configures initial interface panel settings, such as button visibility.
Private Sub InitializePanelSettings
    pnlLayout.SetLayout(0, 0, pnlLayout.Width, pnlLayout.Height)
    pnlLayout.Visible=True
    btnScan.Visible = True
    btnDisconnect.Visible = False
End Sub

' Initializes the dialog with a specific title
Private Sub InitializeDialog
    dialog.Initialize(Root)
    dialog.Title = "AEON 4.0 Setting"
End Sub

' Initializes the BLE manager and updates the initial connection state
Private Sub InitializeManager
    manager.Initialize("manager")
    StateChanged
End Sub

' Updates interface elements based on BLE connection status
Public Sub StateChanged
    If connected Then
        lblDeviceStatus.Text = "Connected: " & ConnectedName
        manager.ReadData2(UUID("FFE0"), UUID("FFE1"))
        Sleep(1000)
        btnScan.Visible = False
        btnDisconnect.Visible = True
        manager.SetNotify(UUID("FFE0"), UUID("FFE1"),True)
    Else
        lblDeviceStatus.Text = "Not connected"
        btnScan.Visible = True
        btnDisconnect.Visible = False
    End If
 
    btnDisconnect.Enabled = connected
    btnScan.Enabled = Not(connected)
    pbScan.Hide
    btnScan.Enabled = (currentState = manager.STATE_POWERED_ON) And connected = False
End Sub

' Configures gauges with initial values and visualization settings.
Private Sub ConfigureGauges
    Dim initialSpeedValues As List = Array(60, 60, 90, 100, 70, 150, 240, 260)
    xSpeedGauge.Value = 0
    xFuelGauge.Value = 0
    xTiltGauge.Value = 60
    xRPMGauge.Value = 0
 
    bmpSpeedview.Initialize(LoadBitmap(File.DirAssets, "backspeedchart.png"))
    speedview.StrokeColor = Colors.Red
    speedview.MaximumNumberOfValues = 10
    speedview.MaxValue = 260
    speedview.MinValue = 0

    For Each value As Int In initialSpeedValues
        speedview.addValue(value)
    Next
End Sub

Private Sub StartDeviceOrientationListening
    PO.StartListening("PO")
End Sub

Private Sub ConfigureFontsAndColors()
    Dim fonts As List = Array(fntTANK, fntOIL, fntTEMP, fntBATT, fntCHOKE, fntBREAK, fntABS, fntLEFT, fntRIGHT, fntLOWBEAM, fntHIGHBEAM, fntDEFAULT)
    For Each fnt As B4XView In fonts
        fnt.Font = xui.CreateFont(Typeface.CreateNew(Typeface.LoadFromAssets("fiveaces.ttf"), Typeface.STYLE_NORMAL), fnt.TextSize)
        fnt.TextColor = xui.Color_Gray
        fnt.Color = xui.Color_Transparent
    Next
 
    Dim labelTexts As Map = CreateMap(fntTANK: "E", fntOIL: "F", fntTEMP: "K", fntBATT: "C", fntCHOKE: "H", fntBREAK: "D", fntABS: "B", fntLEFT: "L", fntRIGHT: "M", fntLOWBEAM: "A", fntHIGHBEAM: "G", fntDEFAULT: "D")
    For Each key As B4XView In labelTexts.Keys
        key.Text = labelTexts.Get(key)
    Next
End Sub

' Handles the click of the scan button, checking the necessary permissions before starting the scan.
Sub btnScan_Click
    Dim Permissions As List
    Dim phone As Phone
    If phone.SdkVersion >= 31 Then
        Permissions = Array("android.permission.BLUETOOTH_SCAN", "android.permission.BLUETOOTH_CONNECT", rp.PERMISSION_ACCESS_FINE_LOCATION)
    Else
        Permissions = Array(rp.PERMISSION_ACCESS_FINE_LOCATION)
    End If
    For Each per As String In Permissions
        rp.CheckAndRequest(per)
        Wait For B4XPage_PermissionResult (Permission As String, Result As Boolean)
        If Result = False Then
            ToastMessageShow("No permission: " & Permission, True)
            Return
        End If
    Next
 
    pbScan.Show
    StartScan
End Sub

Public Sub StartScan
    If manager.State <> manager.STATE_POWERED_ON Then
        Log("Bluetooth is not powered on.")
        Return
    End If

    CheckBluetoothPermissions
    manager.Scan2(Array(UUID("FFE0")), False)
End Sub

Private Sub CheckBluetoothPermissions()
    Dim Permissions As List = Array("android.permission.BLUETOOTH_SCAN", "android.permission.BLUETOOTH_CONNECT", rp.PERMISSION_ACCESS_FINE_LOCATION)
    For Each Permission As String In Permissions
        rp.CheckAndRequest(Permission)
        Wait For B4XPage_PermissionResult (Permission As String, Result As Boolean)
        If Result = False Then
            xui.MsgboxAsync("Permission denied: " & Permission, "Error")
            Return
        End If
    Next
End Sub

' Manage events related to BLE device discovery.
Sub Manager_DeviceFound (Name As String, Id As String, AdvertisingData As Map, RSSI As Double)
    Log("Found: " & Name & ", " & Id & ", RSSI = " & RSSI & ", " & AdvertisingData) 'ignore
'    If Id = "68:E7:4A:EB:EA:BB" Then
    If Name = "AEON" Then
        ConnectedName = Name
        manager.StopScan
        Log("connecting")
        manager.Connect2(Id, False) 'disabling auto connect can make the connection quicker
    End If
End Sub

' Manage events related to Bluetooth state changes.
Sub Manager_StateChanged (State As Int)
    Select State
        Case manager.STATE_POWERED_OFF
            currentStateText = "POWERED OFF"
        Case manager.STATE_POWERED_ON
            currentStateText = "POWERED ON"
        Case manager.STATE_UNSUPPORTED
            currentStateText = "UNSUPPORTED"
    End Select
 
    currentState = State
    StateChanged
End Sub

' Handles the click of the BLE device disconnect button
Sub btnDisconnect_Click
    manager.Disconnect
    Manager_Disconnected
End Sub

Sub btnReadData_Click
    For Each s As String In ConnectedServices
        manager.ReadData(s)
    Next
End Sub

' Processes data received via BLE, decrypting received messages and triggering appropriate processing.
Sub Manager_DataAvailable (ServiceId As String, Characteristics As Map)
    Dim BC As ByteConverter
    For Each id As String In Characteristics.Keys
        Dim b() As Byte = Characteristics.Get(id)
        Dim GetHexString As String = BC.HexFromBytes(b)

        ' Log original hex string
        Log("Trame reçue: " & GetHexString)
     
        ' Split the received string into valid messages
        Dim messages() As String = Regex.Split("0D0A", GetHexString)
        For Each msg As String In messages
            If msg.Length > 0 Then ' Check to ensure non-empty
                Dim validMsg As String = msg & "0D0A" ' Append back the split pattern to each valid message
                'Log("Individual frame: " & validMsg)
                If validMsg.EndsWith("0D0A") Then
                    HandleCharacteristic(validMsg)
                Else
'                    Log("Invalid or incomplete individual frame")
                End If
            End If
        Next
    Next
End Sub

' These methods break down and process specific BLE messages received,
' adjusting gauge values and displaying device data like speed, battery status, etc.
Sub HandleCharacteristic(GetHexString As String)
    ' Vérifiez la longueur et l'intégrité de base de GetHexString
    If GetHexString.EndsWith("0D0A") Then
        ' Cuts the frame according to the specific requirements of each message
        Select Case True
            Case GetHexString.Length = 20 ' For the majority of cases
                ProcessMessage(GetHexString)
             
            Case GetHexString.Length = 24 ' Specific case, as for ODO
                If GetHexString.SubString2(8, 12) = "0032" Then
                    lblODO.Text = "ODO : " & Bit.ParseInt(GetHexString.SubString2(12, 20), 16)
'                    dBSQL.ExecNonQuery("UPDATE counter_set set mileage ='"& Bit.ParseInt(GetHexString.SubString2(12, 20), 16) &"' WHERE ID = 1" )
                End If
             
            Case Else
                Log("Unmanaged or incorrect frame length: " & GetHexString)
        End Select
    Else
        Log("Corrupted or incomplete frame: " & GetHexString)
    End If
End Sub

' These methods break down and process specific BLE messages received,
' adjusting gauge values and displaying device data like speed, battery status, etc.
Sub ProcessMessage(GetHexString As String)
Dim functionCode As String = GetHexString.SubString2(8, 12)
Dim value As String = GetHexString.SubString2(12, 16)
 
Select Case functionCode
            Case "0010" 'SPEED
            Dim speedValue As Int = Bit.ParseInt(value, 16)
            xSpeedGauge.Value = speedValue
            speedview.addValue(speedValue)
         
            Case "0021" 'TRIP
            lblTRIP.Text = "TRIP : " & Bit.ParseInt(value, 16)
         
            Case "0050" 'GEAR
            Select Case Bit.ParseInt(value, 16)
                    Case 0
                        lblGear.TextColor=xui.Color_Green
                        lblGear.Text = "N"
                    Case 1
                        lblGear.TextColor=xui.Color_ARGB(255,249,180,121)
                        lblGear.Text = "1"
                    Case 2
                        lblGear.TextColor=xui.Color_ARGB(255,249,180,121)
                        lblGear.Text = "2"
                    Case 3
                        lblGear.TextColor=xui.Color_ARGB(255,249,180,121)
                        lblGear.Text = "3"
                    Case 4
                        lblGear.TextColor=xui.Color_ARGB(255,249,180,121)
                        lblGear.Text = "4"
                    Case 5
                        lblGear.TextColor=xui.Color_ARGB(255,249,180,121)
                        lblGear.Text = "5"
                    Case 6
                        lblGear.TextColor=xui.Color_ARGB(255,249,180,121)
                        lblGear.Text = "6"
                    Case Else
                        ' This case might never be reached due to the above modulo operation
                End Select
             
            Case "0100" 'RPM
                xRPMGauge.Value = Round(Bit.ParseInt(value, 16)/21.25)
            Case "0110" 'free
                '***********************
            Case "0111" 'DEFAULT
                If Bit.ParseInt(value, 16) = 1 Then
                    fntDEFAULT.TextColor=xui.Color_Red
                Else
                    fntDEFAULT.TextColor=xui.Color_Gray
                End If
             
            Case "0112" 'RIGHT
                If Bit.ParseInt(value, 16) = 1 Then
                    fntRIGHT.TextColor=xui.Color_Green
                Else
                    fntRIGHT.TextColor=xui.Color_Gray
                End If
             
            Case "0113" 'LEFT
                If Bit.ParseInt(value, 16) = 1 Then
                    fntLEFT.TextColor=xui.Color_Green
                Else
                    fntLEFT.TextColor=xui.Color_Gray
                End If
             
            Case "0114" 'HBEAM
                If Bit.ParseInt(value, 16) = 1 Then
                    fntHIGHBEAM.TextColor=xui.Color_Blue
                Else
                    fntHIGHBEAM.TextColor=xui.Color_Gray
                End If
             
            Case "0115" 'LBEAM
                If Bit.ParseInt(value, 16) = 1 Then
                    fntLOWBEAM.TextColor=xui.Color_Yellow
                Else
                    fntLOWBEAM.TextColor=xui.Color_Gray
                End If
             
            Case "0116" 'ABS
                If Bit.ParseInt(value, 16) = 1 Then
                    fntABS.TextColor=xui.Color_ARGB(255,249,180,121)
                Else
                    fntABS.TextColor=xui.Color_Gray
                End If
             
            Case "0117" 'OIL
                If Bit.ParseInt(value, 16) = 1 Then
                    fntOIL.TextColor=xui.Color_Red
                Else
                    fntOIL.TextColor=xui.Color_Gray
                End If
             
            Case "0118" 'FUEL
                If Bit.ParseInt(value, 16) = 1 Then
                    fntTANK.TextColor=xui.Color_ARGB(255,249,180,121)
                Else
                    fntTANK.TextColor=xui.Color_Gray
                End If
             
            Case "0119" 'CHOKE
                If Bit.ParseInt(value, 16) = 1 Then
                    fntCHOKE.TextColor=xui.Color_Yellow
                Else
                    fntCHOKE.TextColor=xui.Color_Gray
                End If
             
            Case "0120" 'TEMP
                If Bit.ParseInt(value, 16) = 1 Then
                    fntTEMP.TextColor=xui.Color_Red
                Else
                    fntTEMP.TextColor=xui.Color_Gray
                End If
             
            Case "0121" 'BATTERY
                If Bit.ParseInt(value, 16) = 1 Then
                    fntBATT.TextColor=xui.Color_Red
                Else
                    fntBATT.TextColor=xui.Color_Gray
                End If
             
            Case "0122" 'METRIC
                If Bit.ParseInt(value, 16) = 1 Then
                    xSpeedGauge.GaugeUnit="mph"
                Else
                    xSpeedGauge.GaugeUnit="km/h"
                End If
         
         
            Case "0123" 'GAUGE %
             
            Case "0130" 'FUEL GAUGE
            xFuelGauge.Value = Round(Bit.ParseInt(value, 16) * (21/255)) 'value * 21 / 255
         
            Case "0140" 'TEMP GAUGE
                '
            Case Else
            ' This case might never be reached due to the above modulo operation
        End Select
End Sub

' Calls the 'StateChanged' method to update the UI to reflect the disconnected state.
Sub Manager_Disconnected
    Log("Disconnected")
    connected = False
    StateChanged
End Sub

' Calls the 'StateChanged' method to update the UI to reflect the connected state.
Sub Manager_Connected (services As List)
    Log("Connected")
    connected = True
    ConnectedServices = services
    Log(services)
    StateChanged
End Sub

'utility to convert short UUIDs to long format on Android
Private Sub UUID(id As String) As String 'ignore
    Return "0000" & id.ToLowerCase & "-0000-1000-8000-00805f9b34fb"
End Sub

' Handles button click related to trips/trips, writes specific data via BLE if connected.
Private Sub bt_trip_Click
    If lblDeviceStatus.Text = "Not connected" Then
        Message
    Else
        manager.WriteData(UUID("FFE0"), UUID("FFE1"),Array As Byte(0xF3))
        lblTRIP.Text = "ODO : 0"
    End If
End Sub

private Sub Message
    xui.MsgboxAsync("You must be paired with the AEON interface to be able to communicate with it","")
End Sub

' Updates the tilt gauge based on changes in device orientation.
Sub PO_OrientationChanged(Azimuth As Float, Pitch As Float, Roll As Float)
    xTiltGauge.Value = 60 - Pitch
End Sub
 
Last edited:

Mr Blue Sky

Active Member
Licensed User
Longtime User
Interesting, can you share your code for other users ?
I am glad to see that you are using the xGauges library.
Hello Klaus, don't you remember that xGauges library, you originally made it for me, thank you again.
 

klaus

Expert
Licensed User
Longtime User
Hello Klaus, don't you remember that xGauges library, you originally made it for me, thank you again.
Yes I remembered it, but only after having posted my comment.
 

Mr Blue Sky

Active Member
Licensed User
Longtime User
Interesting, can you share your code for other users ?
I am glad to see that you are using the xGauges library.

board.jpg


SERVICE_UUID and CHARACTERISTIC_UUID
manager.ReadData2(UUID("FFE0"), UUID("FFE1"))
receive the data:
Sub Manager_DataAvailable (ServiceId As String, Characteristics As Map)
    Dim BC As ByteConverter
    For Each id As String In Characteristics.Keys
        Dim b() As Byte = Characteristics.Get(id)
        Dim GetHexString As String = BC.HexFromBytes(b)

        ' Split the received string into valid messages
        Dim messages() As String = Regex.Split("0D0A", GetHexString)
        For Each msg As String In messages
            If msg.Length > 0 Then ' Check to ensure non-empty
                Dim validMsg As String = msg & "0D0A" ' Append back the split pattern to each valid message
                If validMsg.EndsWith("0D0A") Then
                    HandleCharacteristic(validMsg)
                Else
                    Log("Invalid or incomplete individual frame")
                End If
            End If
        Next
    Next
End Sub

Sub HandleCharacteristic(GetHexString As String)
    ' Check the basic length and integrity of GetHexString
    If GetHexString.EndsWith("0D0A") Then
        ' Cuts the frame according to the specific requirements of each message
        Select Case True
            Case GetHexString.Length = 20 ' For the majority of cases
                ProcessMessage(GetHexString)
         
            Case GetHexString.Length = 24 ' Specific case, as for ODO
                If GetHexString.SubString2(8, 12) = "0032" Then
                    lblODO.Text = "ODO : " & Bit.ParseInt(GetHexString.SubString2(12, 20), 16)
                End If
         
            Case Else
                Log("Unhandled or incorrect frame length: " & GetHexString)
        End Select
    Else
        Log("Corrupted or incomplete frame: " & GetHexString)
    End If
End Sub

Sub ProcessMessage(GetHexString As String)
Dim functionCode As String = GetHexString.SubString2(8, 12)
Dim value As String = GetHexString.SubString2(12, 16)
 
Select Case functionCode
            Case "0010" 'SPEED
            Dim speedValue As Int = Bit.ParseInt(value, 16)
            ]xSpeedGauge.Value = speedValue
            speedview.addValue(speedValue)
     
            Case "0021" 'TRIP
            lblTRIP.Text = "TRIP : " & Bit.ParseInt(value, 16)
         
            Case "0100" 'RPM
                xRPMGauge.Value = Round(Bit.ParseInt(value, 16)/21.25)
     
                ' ** the rest of the functions here  **

            Case Else
                ' This case might never be reached due to the above modulo operation
        End Select
End Sub

hex address:
[B]DISPLAY INDICATOR[/B]
hbeam indicator   -> 0x5A, 0xA5, 0x05, 0x82, 0x01, 0x14, 0x00, 0x??     0x00 or 0x01
lbeam indicator    -> 0x5A, 0xA5, 0x05, 0x82, 0x01, 0x15, 0x00, 0x??     0x00 or 0x01
default indicator   -> 0x5A, 0xA5, 0x05, 0x82, 0x01, 0x11, 0x00, 0x??     0x00 or 0x01
abs indicator        -> 0x5A, 0xA5, 0x05, 0x82, 0x01, 0x16, 0x00, 0x??     0x00 or 0x01
choke indicator    -> 0x5A, 0xA5, 0x05, 0x82, 0x01, 0x19, 0x00, 0x??     0x00 or 0x01
oil indicator         -> 0x5A, 0xA5, 0x05, 0x82, 0x01, 0x17, 0x00, 0x??     0x00 or 0x01
temp indicator    -> 0x5A, 0xA5, 0x05, 0x82, 0x01, 0x20, 0x00, 0x??     0x00 or 0x01
battery indicator  -> 0x5A, 0xA5, 0x05, 0x82, 0x01, 0x21, 0x00, 0x??     0x00 or 0x01
fuel indicator       -> 0x5A, 0xA5, 0x05, 0x82, 0x01, 0x18, 0x00, 0x??     0x00 or 0x01
left indicator        -> 0x5A, 0xA5, 0x05, 0x82, 0x01, 0x13, 0x00, 0x??     0x00 or 0x01
right indicator     -> 0x5A, 0xA5, 0x05, 0x82, 0x01, 0x12, 0x00, 0x??     0x00 or 0x01
gear indicator      -> 0x5A, 0xA5, 0x05, 0x82, 0x00, 0x50, 0x00, 0x??     0x00 to 0x06
fuel gauge           -> 0x5A, 0xA5, 0x05, 0x82, 0x01, 0x30, 0x00, 0x??     0x00 to 0xFF
fuel gauge %      -> 0x5A, 0xA5, 0x05, 0x82, 0x01, 0x23, 0x00, 0x??     0x00 to 0xFF
rpm                    -> 0x5A, 0xA5, 0x05, 0x82, 0x01, 0x00, 0x00, 0x??     0x00 to 0xFF
speed                 -> 0x5A, 0xA5, 0x05, 0x82, 0x00, 0x10, 0x00, 0x??     0x00 to 0xFF
metric                -> 0x5A, 0xA5, 0x05, 0x82, 0x01, 0x22, 0x00, 0x??     0x00 km/h 0xFF mph
trip                     -> 0x5A, 0xA5, 0x05, 0x82, 0x00, 0x21, 0x??, 0x??     0x00 to 0x03E7
odo                    -> 0x5A, 0xA5, 0x05, 0x82, 0x00, 0x32, 0x00, 0x??, 0x??, 0x??    0x00 to 0x0F423F

[B]DISPLAY SETUP[/B]
Circumference     -> 0x5A, 0xA5, 0x05, 0x82, 0x00, 0x90, 0x00, 0x??     0x00 to 0xFF
Pulse                    -> 0x5A, 0xA5, 0x05, 0x82, 0x00, 0x91, 0x00, 0x??     0x00 to 0x0C
Revolution           -> 0x5A, 0xA5, 0x05, 0x82, 0x00, 0x92, 0x00, 0x??     0x00 to 0x04
Metric                  -> 0x5A, 0xA5, 0x05, 0x82, 0x00, 0x95, 0x00, 0x??     0x00 or 0x01
Mileage               -> 0x5A, 0xA5, 0x05, 0x82, 0x00, 0x99, 0x00, 0x??, 0x??, 0x??    0x00 to 0x0F423F
GaugeFuel          -> 0x5A, 0xA5, 0x05, 0x82, 0x00, 0x93, 0x00, 0x??     0x00 to 0xFF
GaugeTemp        -> 0x5A, 0xA5, 0x05, 0x82, 0x00, 0x94, 0x00, 0x??     0x00 to 0xFF

[B]DISPLAY SCREEN[/B]
slide screen       -> 0x5A, 0xA5, 0x04, 0x80, 0x03, 0x00, 0x??   0x00 to 0x03
setup screen      -> 0x5A, 0xA5, 0x04, 0x80, 0x03, 0x00, 0x04                                                                            

panel setup       -> 0x5A, 0xA5, 0x05, 0x81, 0x03, 0x02, 0x00, 0x02
Exit and save     -> 0x5A, 0xA5, 0x05, 0x81, 0x03, 0x02, 0x00, 0x00

[B]RECORD SETUP[/B]
Circumference              -> 0x5A, 0xA5, 0x06, 0x83, 0x00, 0x90, 0x01, 0x00, 0x?? 0x00 to 0xFF
Number of pulses        -> 0x5A, 0xA5, 0x06, 0x83, 0x00, 0x91, 0x01, 0x00, 0x?? 0x00 to 0x0C
Engine revolution        -> 0x5A, 0xA5, 0x06, 0x83, 0x00, 0x92, 0x01, 0x00, 0x?? 0x00 to 0x04
Fuel gauge                  -> 0x5A, 0xA5, 0x06, 0x83, 0x00, 0x93, 0x01, 0x00, 0x?? 0x00 to 0xFF
Temperature gauge    -> 0x5A, 0xA5, 0x06, 0x83, 0x00, 0x94, 0x01, 0x00, 0x?? 0x00 to 0xFF
Metric                         -> 0x5A, 0xA5, 0x06, 0x83, 0x00, 0x95, 0x01, 0x00, 0x?? 0x00 or 0x01
 
Last edited:
Hi, Very nice presentation... But your code command structure is for DWIN LCD which are not android base so I think you adopted the same command structure for android app... am I right?
 

Mr Blue Sky

Active Member
Licensed User
Longtime User
Hi, Very nice presentation... But your code command structure is for DWIN LCD which are not android base so I think you adopted the same command structure for android app... am I right?
You also have some sample b4x code for receiving data from the interface to Android via Bluetooth. the hex addresses that follow are the commands transmitted from the interface to the meter or to the telephone.


For the screen Giraffe IDE Materials
 
Last edited:

amorosik

Expert
Licensed User
Dear Mr. Blue Sky, it is possible to say without fear of contradiction that the dashboard presented is truly beautiful
And therefore with this writing, the entire B4x community (?!?!?) kindly asks you to make the necessary sources available to us to achieve the above
Most likely we will never use it, but it will be a pleasure for the eyes to see it running on your PC
😁🤣😇
 
Last edited:
You also have some sample b4x code for receiving data from the interface to Android via Bluetooth. the hex addresses that follow are the commands transmitted from the interface to the meter or to the telephone.
For the screen Giraffe IDE Materials
Wow Really beautiful... Just to request to share some screen design for learning. Thanks.
 
Thanks for sharing info... But still confused that if the LCD is serial port controlled then where is android function in your first post? Did you made it in B4A or B4R !!!
You made it beautifully but how still is mystery o_O
 

Mr Blue Sky

Active Member
Licensed User
Longtime User
Thanks for sharing info... But still confused that if the LCD is serial port controlled then where is android function in your first post? Did you made it in B4A or B4R !!!
You made it beautifully but how still is mystery o_O
a drawing is better than a long speech, it should help understanding
syno.jpg

The objective of my approach is to provide efficient and easy-to-access tools to allow users to develop their own interfaces and dashboards for smartphones. I chose to use B4X for several key reasons:

Accessibility: B4X is designed to be accessible to developers of all levels, including beginners. It avoids the complexity of traditional development environments like Android Studio, making app creation more affordable for those without extensive programming training.

Cross-Platform Compatibility: B4X allows you to develop applications that work on both iOS and Android from a single code base. This simplifies the development process since you don't need to manage two separate projects for each platform.

Speed of development: Thanks to its numerous predefined tools and libraries, B4X considerably reduces the time required for development. Users can quickly create working prototypes and test them on their devices.

Customization: B4X provides sufficient flexibility to customize applications according to the specific needs of each user. Whether to control my box or integrate unique functionalities, B4X allows this adaptation without requiring in-depth technical expertise.

In summary, by offering B4X, within my subscribers and Facebook groups, my objective is to democratize the development of mobile applications linked to my interface, by making the technology more accessible and by giving users the freedom to create personalized solutions. adapted to their needs.
 
Last edited:

amorosik

Expert
Licensed User
a drawing is better than a long speech, it should help understanding
View attachment 154826

Dear Mr Blue, since you insist on showing us all these delicacies, I am the spokesperson for the entire community to ask for the release of the code necessary to reproduce everything
In case of inserting other posts that make us dream without having the possibility of reproducing what is described, a petition will be requested to close this 3D Thanks and goodbye

(in case you don't understand, this post is ironic)
 

Mr Blue Sky

Active Member
Licensed User
Longtime User
@Mr Blue Sky congratulation on developing such a complex system in B4A and giving us the details.
You may not realize but you have created a great curiosity in all the members now.
So ...
Thank you for your appreciation, without B4X and Erel and many contributors none of this would have been possible. Making smartphone application development accessible to as many people as possible
 
Thank you for your appreciation, without B4X and Erel and many contributors none of this would have been possible. Making smartphone application development accessible to as many people as possible
So as many people contribute at B4X, it is nice if you share some demo codes having basic functionality not completed, which someone could learn. I also want to make a display for some sensors using same your idea i.e. embedded display and BLE app. Thanks.
 

Mr Blue Sky

Active Member
Licensed User
Longtime User
View attachment 154761
  • Analog to digital signal conversion:
    • Engine speed signal for 1, 2, 3, or 4 cylinders.
    • Speed signal from a bridge inductive sensor or Hall sensor.
    • Settings for km/h or mph, wheel circumference, number of pulses per revolution.
  • Various indicators:
    • Neutral indicator.
    • Gear position indicator for gears 1 to 6.
    • Fuel reserve indicator.
    • Low battery indicator.
    • Turn signal indicator (right, left, hazard).
    • High beam indicator.
    • Position lights indicator.
    • ABS indicator.
    • Temperature indicator.
    • Oil pressure indicator.
    • Choke indicator.
    • Stop control indicator.
    • Fuel level gauge.
    • Multi-screen display (4 screens).
  • Relay controls:
    • On/off relay control.
    • Timed relay control.
View attachment 154760

Attached is a Windows x64 and x86 application designed to simulate sending data from a PC to a smartphone. Configure your PC's Bluetooth LE to use port 1 to 5 and rename it to 'AEON'. You can then connect your phone to the 'AEON' SSID. Using the simulator you will be able to send data to your phone. If your computer does not have Bluetooth LE you must purchase a USB Bluetooth 4.0 HM-10 or BT-09 compatible adapter

DASHBOARD_SIMULATOR_x64
DASHBOARD_SIMULATOR_x86
Simplified B4A script for demonstrations

AEON DASHBOARD Android
AEON DASHBOARD iOS

Simplified B4A script for demonstrations:
'' This script was written to demonstrate the Bluetooth Low Energy connection with the
'' AEON electronic interface or with PC simulation software. It is intended to provide
'' useful information for the development of various graphics applications in line with
'' the AEON product.
'' Written by André FAËS (Mr Blue Sky)

#Region Shared Files
#CustomBuildAction: folders ready, %WINDIR%\System32\Robocopy.exe,"..\..\Shared Files" "..\Files"
'Ctrl + click to sync files: ide://run?file=%WINDIR%\System32\Robocopy.exe&args=..\..\Shared+Files&args=..\Files&FilesSync=True
#End Region

'Ctrl + click to export as zip: ide://run?File=%B4X%\Zipper.jar&Args=BLEExample.zip

' Class declaration and initialization of global variables
' defines variables used across the class, including UI elements
' like buttons and gauges, and managers for BLE services and execution permissions.
Sub Class_Globals
    Private Root As B4XView
    Private xui As XUI
    Private manager As BleManager2
    Private rp As RuntimePermissions
    Dim PO As PhoneOrientation
    Private pnlLayout As Panel
    Private btnDisconnect As B4XView
    Private btnScan As B4XView
    Private lblDeviceStatus As B4XView
    Private currentStateText As String = "UNKNOWN"
    Private currentState As Int
    Private connected As Boolean = False
    Private ConnectedName As String = "AEON"
    Private ConnectedServices As List
    Private pbScan As B4XLoadingIndicator
    Private xSpeedGauge As xGauges
    Private xFuelGauge As xGauges
    Private xTiltGauge As xGauges
    Private xRPMGauge As xGauges
    Private fntTANK, fntOIL, fntTEMP, fntBATT, fntCHOKE, fntBREAK, fntABS, fntDEFAULT, fntLEFT, fntRIGHT, fntLOWBEAM, fntHIGHBEAM As B4XView
    Private lblGear, lblODO, lblTRIP As Label
    Private bt_trip As B4XView
    Private dialog As B4XDialog
    Private speedview As SnakeView
    Dim bmpSpeedview As BitmapDrawable
End Sub

' Method provided for initializing additional components or databases if necessary, currently commented
Public Sub Initialize
'    If dBSQL.IsInitialized = False Then
'        InitdbSQL
'    End If
End Sub

' Load the main layout, initialize the dialog, BLE manager, configure gauges,
' start listening for device orientation, and configure policies and colors.
' Each of these methods initializes a specific part of the application,
' such as panel settings, dialogs, handlers, gauges, etc.
Private Sub B4XPage_Created (Root1 As B4XView)
    Root = Root1
    Root.LoadLayout("main")
    InitializePanelSettings
    InitializeDialog
    InitializeManager
    ConfigureGauges
    StartDeviceOrientationListening
    ConfigureFontsAndColors
End Sub

' Configures initial interface panel settings, such as button visibility.
Private Sub InitializePanelSettings
    pnlLayout.SetLayout(0, 0, pnlLayout.Width, pnlLayout.Height)
    pnlLayout.Visible=True
    btnScan.Visible = True
    btnDisconnect.Visible = False
End Sub

' Initializes the dialog with a specific title
Private Sub InitializeDialog
    dialog.Initialize(Root)
    dialog.Title = "AEON 4.0 Setting"
End Sub

' Initializes the BLE manager and updates the initial connection state
Private Sub InitializeManager
    manager.Initialize("manager")
    StateChanged
End Sub

' Updates interface elements based on BLE connection status
Public Sub StateChanged
    If connected Then
        lblDeviceStatus.Text = "Connected: " & ConnectedName
        manager.ReadData2(UUID("FFE0"), UUID("FFE1"))
        Sleep(1000)
        btnScan.Visible = False
        btnDisconnect.Visible = True
        manager.SetNotify(UUID("FFE0"), UUID("FFE1"),True)
    Else
        lblDeviceStatus.Text = "Not connected"
        btnScan.Visible = True
        btnDisconnect.Visible = False
    End If
 
    btnDisconnect.Enabled = connected
    btnScan.Enabled = Not(connected)
    pbScan.Hide
    btnScan.Enabled = (currentState = manager.STATE_POWERED_ON) And connected = False
End Sub

' Configures gauges with initial values and visualization settings.
Private Sub ConfigureGauges
    Dim initialSpeedValues As List = Array(60, 60, 90, 100, 70, 150, 240, 260)
    xSpeedGauge.Value = 0
    xFuelGauge.Value = 0
    xTiltGauge.Value = 60
    xRPMGauge.Value = 0
 
    bmpSpeedview.Initialize(LoadBitmap(File.DirAssets, "backspeedchart.png"))
    speedview.StrokeColor = Colors.Red
    speedview.MaximumNumberOfValues = 10
    speedview.MaxValue = 260
    speedview.MinValue = 0

    For Each value As Int In initialSpeedValues
        speedview.addValue(value)
    Next
End Sub

Private Sub StartDeviceOrientationListening
    PO.StartListening("PO")
End Sub

Private Sub ConfigureFontsAndColors()
    Dim fonts As List = Array(fntTANK, fntOIL, fntTEMP, fntBATT, fntCHOKE, fntBREAK, fntABS, fntLEFT, fntRIGHT, fntLOWBEAM, fntHIGHBEAM, fntDEFAULT)
    For Each fnt As B4XView In fonts
        fnt.Font = xui.CreateFont(Typeface.CreateNew(Typeface.LoadFromAssets("fiveaces.ttf"), Typeface.STYLE_NORMAL), fnt.TextSize)
        fnt.TextColor = xui.Color_Gray
        fnt.Color = xui.Color_Transparent
    Next
 
    Dim labelTexts As Map = CreateMap(fntTANK: "E", fntOIL: "F", fntTEMP: "K", fntBATT: "C", fntCHOKE: "H", fntBREAK: "D", fntABS: "B", fntLEFT: "L", fntRIGHT: "M", fntLOWBEAM: "A", fntHIGHBEAM: "G", fntDEFAULT: "D")
    For Each key As B4XView In labelTexts.Keys
        key.Text = labelTexts.Get(key)
    Next
End Sub

' Handles the click of the scan button, checking the necessary permissions before starting the scan.
Sub btnScan_Click
    Dim Permissions As List
    Dim phone As Phone
    If phone.SdkVersion >= 31 Then
        Permissions = Array("android.permission.BLUETOOTH_SCAN", "android.permission.BLUETOOTH_CONNECT", rp.PERMISSION_ACCESS_FINE_LOCATION)
    Else
        Permissions = Array(rp.PERMISSION_ACCESS_FINE_LOCATION)
    End If
    For Each per As String In Permissions
        rp.CheckAndRequest(per)
        Wait For B4XPage_PermissionResult (Permission As String, Result As Boolean)
        If Result = False Then
            ToastMessageShow("No permission: " & Permission, True)
            Return
        End If
    Next
 
    pbScan.Show
    StartScan
End Sub

Public Sub StartScan
    If manager.State <> manager.STATE_POWERED_ON Then
        Log("Bluetooth is not powered on.")
        Return
    End If

    CheckBluetoothPermissions
    manager.Scan2(Array(UUID("FFE0")), False)
End Sub

Private Sub CheckBluetoothPermissions()
    Dim Permissions As List = Array("android.permission.BLUETOOTH_SCAN", "android.permission.BLUETOOTH_CONNECT", rp.PERMISSION_ACCESS_FINE_LOCATION)
    For Each Permission As String In Permissions
        rp.CheckAndRequest(Permission)
        Wait For B4XPage_PermissionResult (Permission As String, Result As Boolean)
        If Result = False Then
            xui.MsgboxAsync("Permission denied: " & Permission, "Error")
            Return
        End If
    Next
End Sub

' Manage events related to BLE device discovery.
Sub Manager_DeviceFound (Name As String, Id As String, AdvertisingData As Map, RSSI As Double)
    Log("Found: " & Name & ", " & Id & ", RSSI = " & RSSI & ", " & AdvertisingData) 'ignore
'    If Id = "68:E7:4A:EB:EA:BB" Then
    If Name = "AEON" Then
        ConnectedName = Name
        manager.StopScan
        Log("connecting")
        manager.Connect2(Id, False) 'disabling auto connect can make the connection quicker
    End If
End Sub

' Manage events related to Bluetooth state changes.
Sub Manager_StateChanged (State As Int)
    Select State
        Case manager.STATE_POWERED_OFF
            currentStateText = "POWERED OFF"
        Case manager.STATE_POWERED_ON
            currentStateText = "POWERED ON"
        Case manager.STATE_UNSUPPORTED
            currentStateText = "UNSUPPORTED"
    End Select
 
    currentState = State
    StateChanged
End Sub

' Handles the click of the BLE device disconnect button
Sub btnDisconnect_Click
    manager.Disconnect
    Manager_Disconnected
End Sub

Sub btnReadData_Click
    For Each s As String In ConnectedServices
        manager.ReadData(s)
    Next
End Sub

' Processes data received via BLE, decrypting received messages and triggering appropriate processing.
Sub Manager_DataAvailable (ServiceId As String, Characteristics As Map)
    Dim BC As ByteConverter
    For Each id As String In Characteristics.Keys
        Dim b() As Byte = Characteristics.Get(id)
        Dim GetHexString As String = BC.HexFromBytes(b)

        ' Log original hex string
        Log("Trame reçue: " & GetHexString)
     
        ' Split the received string into valid messages
        Dim messages() As String = Regex.Split("0D0A", GetHexString)
        For Each msg As String In messages
            If msg.Length > 0 Then ' Check to ensure non-empty
                Dim validMsg As String = msg & "0D0A" ' Append back the split pattern to each valid message
                'Log("Individual frame: " & validMsg)
                If validMsg.EndsWith("0D0A") Then
                    HandleCharacteristic(validMsg)
                Else
'                    Log("Invalid or incomplete individual frame")
                End If
            End If
        Next
    Next
End Sub

' These methods break down and process specific BLE messages received,
' adjusting gauge values and displaying device data like speed, battery status, etc.
Sub HandleCharacteristic(GetHexString As String)
    ' Check the basic length and integrity of GetHexString
    If GetHexString.EndsWith("0D0A") Then
        ' Cuts the frame according to the specific requirements of each message
        Select Case True
            Case GetHexString.Length = 20 ' For the majority of cases
                ProcessMessage(GetHexString)
             
            Case GetHexString.Length = 24 ' Specific case, as for ODO
                If GetHexString.SubString2(8, 12) = "0032" Then
                    lblODO.Text = "ODO : " & Bit.ParseInt(GetHexString.SubString2(12, 20), 16)
'                    dBSQL.ExecNonQuery("UPDATE counter_set set mileage ='"& Bit.ParseInt(GetHexString.SubString2(12, 20), 16) &"' WHERE ID = 1" )
                End If
             
            Case Else
                Log("Unmanaged or incorrect frame length: " & GetHexString)
        End Select
    Else
        Log("Corrupted or incomplete frame: " & GetHexString)
    End If
End Sub

' These methods break down and process specific BLE messages received,
' adjusting gauge values and displaying device data like speed, battery status, etc.
Sub ProcessMessage(GetHexString As String)
Dim functionCode As String = GetHexString.SubString2(8, 12)
Dim value As String = GetHexString.SubString2(12, 16)
 
Select Case functionCode
            Case "0010" 'SPEED
            Dim speedValue As Int = Bit.ParseInt(value, 16)
            xSpeedGauge.Value = speedValue
            speedview.addValue(speedValue)
         
            Case "0021" 'TRIP
            lblTRIP.Text = "TRIP : " & Bit.ParseInt(value, 16)
         
            Case "0050" 'GEAR
            Select Case Bit.ParseInt(value, 16)
                    Case 0
                        lblGear.TextColor=xui.Color_Green
                        lblGear.Text = "N"
                    Case 1
                        lblGear.TextColor=xui.Color_ARGB(255,249,180,121)
                        lblGear.Text = "1"
                    Case 2
                        lblGear.TextColor=xui.Color_ARGB(255,249,180,121)
                        lblGear.Text = "2"
                    Case 3
                        lblGear.TextColor=xui.Color_ARGB(255,249,180,121)
                        lblGear.Text = "3"
                    Case 4
                        lblGear.TextColor=xui.Color_ARGB(255,249,180,121)
                        lblGear.Text = "4"
                    Case 5
                        lblGear.TextColor=xui.Color_ARGB(255,249,180,121)
                        lblGear.Text = "5"
                    Case 6
                        lblGear.TextColor=xui.Color_ARGB(255,249,180,121)
                        lblGear.Text = "6"
                    Case Else
                        ' This case might never be reached due to the above modulo operation
                End Select
             
            Case "0100" 'RPM
                xRPMGauge.Value = Round(Bit.ParseInt(value, 16)/21.25)
            Case "0110" 'free
                '***********************
            Case "0111" 'DEFAULT
                If Bit.ParseInt(value, 16) = 1 Then
                    fntDEFAULT.TextColor=xui.Color_Red
                Else
                    fntDEFAULT.TextColor=xui.Color_Gray
                End If
             
            Case "0112" 'RIGHT
                If Bit.ParseInt(value, 16) = 1 Then
                    fntRIGHT.TextColor=xui.Color_Green
                Else
                    fntRIGHT.TextColor=xui.Color_Gray
                End If
             
            Case "0113" 'LEFT
                If Bit.ParseInt(value, 16) = 1 Then
                    fntLEFT.TextColor=xui.Color_Green
                Else
                    fntLEFT.TextColor=xui.Color_Gray
                End If
             
            Case "0114" 'HBEAM
                If Bit.ParseInt(value, 16) = 1 Then
                    fntHIGHBEAM.TextColor=xui.Color_Blue
                Else
                    fntHIGHBEAM.TextColor=xui.Color_Gray
                End If
             
            Case "0115" 'LBEAM
                If Bit.ParseInt(value, 16) = 1 Then
                    fntLOWBEAM.TextColor=xui.Color_Yellow
                Else
                    fntLOWBEAM.TextColor=xui.Color_Gray
                End If
             
            Case "0116" 'ABS
                If Bit.ParseInt(value, 16) = 1 Then
                    fntABS.TextColor=xui.Color_ARGB(255,249,180,121)
                Else
                    fntABS.TextColor=xui.Color_Gray
                End If
             
            Case "0117" 'OIL
                If Bit.ParseInt(value, 16) = 1 Then
                    fntOIL.TextColor=xui.Color_Red
                Else
                    fntOIL.TextColor=xui.Color_Gray
                End If
             
            Case "0118" 'FUEL
                If Bit.ParseInt(value, 16) = 1 Then
                    fntTANK.TextColor=xui.Color_ARGB(255,249,180,121)
                Else
                    fntTANK.TextColor=xui.Color_Gray
                End If
             
            Case "0119" 'CHOKE
                If Bit.ParseInt(value, 16) = 1 Then
                    fntCHOKE.TextColor=xui.Color_Yellow
                Else
                    fntCHOKE.TextColor=xui.Color_Gray
                End If
             
            Case "0120" 'TEMP
                If Bit.ParseInt(value, 16) = 1 Then
                    fntTEMP.TextColor=xui.Color_Red
                Else
                    fntTEMP.TextColor=xui.Color_Gray
                End If
             
            Case "0121" 'BATTERY
                If Bit.ParseInt(value, 16) = 1 Then
                    fntBATT.TextColor=xui.Color_Red
                Else
                    fntBATT.TextColor=xui.Color_Gray
                End If
             
            Case "0122" 'METRIC
                If Bit.ParseInt(value, 16) = 1 Then
                    xSpeedGauge.GaugeUnit="mph"
                Else
                    xSpeedGauge.GaugeUnit="km/h"
                End If
         
         
            Case "0123" 'GAUGE %
             
            Case "0130" 'FUEL GAUGE
            xFuelGauge.Value = Round(Bit.ParseInt(value, 16) * (21/255)) 'value * 21 / 255
         
            Case "0140" 'TEMP GAUGE
                '
            Case Else
            ' This case might never be reached due to the above modulo operation
        End Select
End Sub

' Calls the 'StateChanged' method to update the UI to reflect the disconnected state.
Sub Manager_Disconnected
    Log("Disconnected")
    connected = False
    StateChanged
End Sub

' Calls the 'StateChanged' method to update the UI to reflect the connected state.
Sub Manager_Connected (services As List)
    Log("Connected")
    connected = True
    ConnectedServices = services
    Log(services)
    StateChanged
End Sub

'utility to convert short UUIDs to long format on Android
Private Sub UUID(id As String) As String 'ignore
    Return "0000" & id.ToLowerCase & "-0000-1000-8000-00805f9b34fb"
End Sub

' Handles button click related to trips/trips, writes specific data via BLE if connected.
Private Sub bt_trip_Click
    If lblDeviceStatus.Text = "Not connected" Then
        Message
    Else
        manager.WriteData(UUID("FFE0"), UUID("FFE1"),Array As Byte(0xF3))
        lblTRIP.Text = "ODO : 0"
    End If
End Sub

private Sub Message
    xui.MsgboxAsync("You must be paired with the AEON interface to be able to communicate with it","")
End Sub

' Updates the tilt gauge based on changes in device orientation.
Sub PO_OrientationChanged(Azimuth As Float, Pitch As Float, Roll As Float)
    xTiltGauge.Value = 60 - Pitch
End Sub
 
Last edited:
Top