The metrics subscription scheme is
an option, and the auto-subscribe
feature via a getter is a nice idea. But
I wouldn't apply that to metrics value
conversions and units.
Also we would still need the current set
of metrics to be subscribed by default,
as there are also non Javascript devices
(e.g. smart buttons, Wifi displays)
reading the WebSocket stream.
My thoughts on this so far:
Basically the web UI, as any frontend,
should adapt to unit configurations
seamlessly. The web UI includes many
command outputs, which already
automatically switch units as
configured.
For all practical purposes, the web UI
needs to interact with users in their
preferred units. Only some plugins and
functions will need certain values in
metric (native) units for calculations,
and these will also need a simple way to
convert calculation results back to user
units for displaying. So I think we need
to provide the unit configuration and
value conversion tools in the web
framework as well.
Proposal:
a) We provide a config option and a
WebSocket command to switch the
WebSocket metrics transmission mode to
user / native units. To keep plugin
compatibility, the default is 'native'.
b) We introduce a separate units
dictionary object containing the user
units for both metrics and the unit
groups in their code & label
representation. The units dictionary
only needs to be sent initially, when
new metrics are registered, when the
metrics mode is changed for the current
connection, and when some user unit
configuration is changed, keeping the
bandwidth and processing requirements
low.
The units dictionary can combine both
metrics and group units, as the unit
group names are fully distinct from the
metrics namespace. The transport scheme
could be:
{ units: {
"v.p.speed": { code: "kmph", label:
"km/h" }, …
"units.distance":
{ code: "miles",
label: "M" },
…
}
c) In the web framework,
accessing units should be as simple as
possible and avoid throwing exceptions
for undefined entries, so we could e.g.
split these into separate code &
label objects:
units["v.p.speed"] =
"km/h" // consistently
accompanies metrics["v.p.speed"]
unitcodes["v.p.speed"]
= "kmph"
units["units.distance"]
= "M"
unitcodes["units.distance"]
= "miles"
…or provide a getter that tests for the
key existence and returns an object with
empty fields as necessary.
With this, all metrics displays can
easily be changed to display the actual
unit labels instead of using fixed
strings.
d) To provide value conversion we
implement UnitConvert() in Javascript
plus some wrappers that automatically
look up the unit for a given
metrics/group name and do the conversion
to/from native units, something like…
var speed_kph
= toNativeValue("v.p.speed"); //
optional second arg to convert any
data
var speed_kph
= metrics_native["v.p.speed"]; //
using a getter
var trip_display =
toUserValue("units.distance", 1234);
Plugins for scientific/technical
applications that depend on native
(metric) units can use the new metrics
transmission mode control command to
force native mode. Or they can choose to
migrate from "metrics[]" to
"metrics_native[]".
The metrics mode config option can come
with a note informing users that there
may be some old plugins not compatible
with non-native units. They can then
check their plugins for this and make an
informed decision on wether to enable
user units and/or wether to install a
specific plugin.
Thoughts, comments?
Regards,
Michael
Am 25.11.22 um 03:13 schrieb
Michael Geddes:
I have an idea which
might reduce traffic for
maintaining the metrics[] array in
the browser and cope with the user
units.
I'll start by saying
I'm not a JS developer per se.. so
a newb in JS really. Still, it's
mainly just another language so ..
we'll give it a go.
Firstly:
* Implement the 'changed'
filters as below for the
web-socket.. for both normal and
'user' values.
* Add a function that
subscribes to a value (and
returns the current value of
it)..including to 'user'
value/unitlabel.
Subscribing the normal way to
the metrics over the websocket
would have the normal effect..
but we would have a new way that
would subscribe in a filtered
way.
I've had a little play with
the Proxy object .. so at least
I know this should work:
Have a metrics_ array that is
the real associative array for
metrics[] and then define a
Proxy that has (at the least)
'get' and 'has' defined (giving
us the ability to overload metrics['prop']
and "prop" in metrics
operations).
The get function
would return the underlying
value if it exists in the metrics_ array
(which is maintained through
the websocket from currently
subscribed values in the current
manner).
If the value is not in the metrics_ array
- it would then do a
subscribe+query on the websocket
getting the current value and
adding it into the metrics_ container.
If it was unavailable then it
would put undefined into
the array.
The 'has' would do the get()
and return true if the value was
not == undefined.
For the 'query
the websocket' bit, I'm
assuming I would be working
with promises or futures or
some such: I'll do the
research and do it properly
unless somebody can help me
out with it. That's the bit I
was going to work on next for
the proof-of-concept.
Any immediate thoughts?
Dangers?
I also noticed there was a
bit that went through html
element properties and looked
for metrics .. this could be
used to bulk subscribe to any
metric values required there.
//.ichael
Yeah, ok.
I will get all the other
'user unit' stuff done as a
line in the sand, and then
move to working out the web
stuff. I'm still finding my
way though all the client
side javascript, which looks
very cool.. but I've not
really done jQuery before
(just enough to recognise
it).
Subscribing to metrics
with/without user units
makes a lot of sense.
Obviously the default needs
to be 'Subscribe to all
metrics but not user units'
to maintain compatibility...
but I was also thinking it
might be nice if we could
filter down even the normal
subscribed events.
We could have:
* Web socket command to
filter units (flag on
websocket to say 'filtered'
+ flag bitset on each
metric similar to 'dirty')
Then either:
* Web socket command to
turn on user units (single
flag on that websocket)
or
* Web socket command to
turn on user units for
specific metrics (flag
bitset on each metric)
A parameter to the URI
for the websocket could
start the socket in
'filtered' mode to avoid the
initial rush of metrics.
This could drastically
reduce traffic and time for
the metrics command to
execute. It would be
possible to also check (on a
'filtered' websocket) for
any changes to metrics for
that websocket slot before
queueing the 'metric update'
socket command.
//.ichael
Michael,
I don't have much spare
time currently, just
some quick first
comments: it's important
to implement this as
lightweight as possible,
both in terms of network
load and client CPU
& memory
requirements. Some
devices already have
issues, which can be
seen by the "websocket
overflow" messages. The
web UI also should stay
usable via cellular.
My impression is the new
scheme, while only
slightly raising the
client requirements,
adds substantially to
the network
requirements.
An option could be to
separate the units -- or
more, back when
implementing this I
thought about separating
the names later on.
Another question is if
we normally generally
need both the native and
the converted values in
the web UI. We maybe
could provide an option
to switch to converted
values, or add an option
to retreive or subscribe
to a set of converted
metrics on demand.
Standard plugins like
ABRP and PwrMon rely on
getting metric (native)
units, and there
probably are non-public
plugins, e.g. for
engineering &
scientific projects,
that depend on metric
units to do their
calculations and don't
need anything else. We
shouldn't make life
harder for these
applications without
good reason.
Regards,
Michael
Am 15.11.22 um
01:26 schrieb Michael
Geddes:
If
you're ok with the
[default] option
I'll stick with
that. I mean in
some ways it would
be nice to have a
button choice
metric
| usa | europe |
asia | custom
etc and I kind of
considered
something like
that but figured
it's only a
handful of
choices.. and it's
an embedded
device.. so
simpler is better.
On a related
note - I was
thinking how it
would be nice if
the dashboard
(etc) had access
to the 'user'
units, so went
hunting down that
little rabbit
hole. Quite a nice
mechanism with the
web socket
updating the
"metrics" object
in the UI.
This is a
snippet of one
idea, which is
that for any
metric that has
the possibility of
a user unit, we
set the extra
values of the
metric with
'#unit' and
'#user' appended -
see below. (I've
chosen '#'
arbitrarily.. but
it could be '/' or
':' or '>' but
maybe not '.' )
v.p.odometer#unit:
"M"
v.p.odometer#user:
6754.91
v.p.satcount: 13
v.p.speed: 0
v.p.speed#unit:
"km/h"
v.p.speed#user: null
v.p.trip: 28
v.p.trip#unit:
"M"
v.p.trip#user:
17.3984
Then we can use
this in the dials
to populate the
values and
captions! (not
that I like
Miles).
I
The other
(similar) way
was to have
something like
the following:
"v.p.trip#user"
: { "value":
17.3984, "unit":
"M" }
It wouldn't
make the total
message any
shorter.. soo..
dunno.
There's also
some
complications
with setting up
the dials (for
min/max values)
- like for the
speed.
Notice also
that I'm
returning null
for undefined
values. It's
nice - but I'm
not sure how
javascript
handles null
when used /
printed etc.
//.ichael
Michael,
looks good.
I think having
an explicit
'default'
option is
better than
taking the
'Metric'
equivalent for
that, as in
your example
you already
show unit
alternatives
within the
metric system
to support
different
scalings (kW /
W, kWh / Wh).
(Btw… waiting
for someone to
miss
Horsepower
& BTU here
;-))
@Patrick, I
think that
also answers
your implicit
question:
The default button makes it unclear what the actual setting is.
The default
(native unit)
is always
metric, but
you may have a
mix of
scalings, as
we try to find
the one that
fits best for
the given
application
when defining
a metric. For
example the
current
driving energy
consumption is
stored
natively in
Wh/km, while
the energy
used or
regenerated is
in kWh, and
the odometer
& trip
counters are
in km, while
the altitude
ist in m.
Regards,
Michael
Am
13.11.22 um
08:42 schrieb
Michael
Geddes:
Greetings,
so this
is my idea of
being able to
select which
units various
groups use (in
addition to
Distance).
This can
be then
accessed by
the special
'user' unit
code. (or
'metrics list
-u ' )
The idea
of [Default]
selection
below simply
means storing
the value to
blank -
meaning use
whatever unit
the particular
metric uses.
The other idea
I had was to
actually
default it to
the equivalent
of 'Metric'
special unit
code and not
have the
[Default]
button.
Currently
I've made it
so that if
there are more
than 3 choices
other than
[default] that
it uses the
choice/combo
box rather
than the Radio
buttons. (ie
this list is
auto-generated
from the
Metric Units
table and the
Metric Groups
table).
Thoughts
/ comments?
//.ichael
https://github.com/openvehicles/Open-Vehicle-Monitoring-System-3/pull/771
I'm
hoping this
P/R is ok in
this form
(made of 5
separate commits).
I will
have a look at
implementing
the "user"
unit code.
The base for
how it
would work is
already a part
of the above
pull request.
I'll just look
at the module
configuration
for distance.
The
'power
consumption'
is one where
it's not just
a check-box..
there're 5
possible
choice!
I should
also add 'bar'
for pressure
given that for
some reason
that's still a
thing people
want.
//.ichael
I think
this is pretty
decent &
complete now.
I also like
the approach
of the 'user'
unit code.
Moving all
user unit
prefs into the
module
configuration
is an old
todo.
Currently only
the distance
unit is
defined at the
module side,
temperature
and pressure
are App prefs.
Regards,
Michael
Am
11.11.22 um
09:54 schrieb
Michael
Geddes:
Ok
- so here's
what I have
implemented
for Duktape
and Metrics.
(I added
IsDefined() as
well).
Any
thoughts on
this?
Noting
OvmsMetrics.Float(
{metric} )
-> Outputs
metric as
float (same)
OvmsMetrics.Float(
{metric},
{unit}) ->
Outputs metric
as float
converted to
given unit
(new)
OvmsMetrics.Value(
{metric} )
-> Outputs
Metric in
native value
(same)
OvmsMetrics.Value(
{metric} ,
false) ->
Outputs Metric
as string and
no units
(same)
OvmsMetrics.Value(
{metric} ,
{unit}) ->
Outputs Metric
converted to
given unit as
native value.
(new)
OvmsMetrics.Value(
{metric} ,
{unit}, false
) ->
Outputs Metric
converted to
given unit as
string
including any
unit
specifier.
(new)
also
OvmsMetric.GetValues(
{metric}
[,{unit}] [,
{converted} ]
) Adds
similar
behaviour to
Value() above.
also the
special units
'imperial'
and 'metric'
will convert
to the
associated
imperial /
metric version
of the units
as
appropriate.
(function()
{
dump =
function
(metric) {
print( metric+
" ["+(typeof
metric)+"]\n"
); }
dump_obj =
function (obj
) {
print('---
Object
----\n')
for (var
k in obj) {
xk =
obj[k];
print(
k+':'+ xk + '
['+typeof xk+
"]\n");
}
}
dump(OvmsMetrics.Value("xiq.v.trip.consumption"));
dump(OvmsMetrics.Value("xiq.v.trip.consumption",
false));
dump(OvmsMetrics.Value("xiq.v.trip.consumption","kmpkwh"));
dump(OvmsMetrics.Value("xiq.v.trip.consumption",
"mipkwh",
false));
dump(OvmsMetrics.AsFloat("xiq.v.trip.consumption"));
dump(OvmsMetrics.AsFloat("xiq.v.trip.consumption","kmpkwh"));
dump(OvmsMetrics.Value("xiq.v.trip.consumption","imperial"))
dump(OvmsMetrics.Value("xiq.v.trip.consumption","imperial",
false))
dump_obj(OvmsMetrics.GetValues("trip",
"metric"))
dump_obj(OvmsMetrics.GetValues("trip",
"imperial",
false))
})();
With this
output:
19.2308 [number]
19.2308 [string]
5.2 [number]
3.23112mi/kWh [string]
19.2308 [number]
5.2 [number]
309.49 [number]
309.49Wh/mi [string]
--- Object ----
v.p.trip:13 [number]
xiq.e.trip:0 [number]
xiq.e.trip.energy.recuperated:0 [number]
xiq.e.trip.energy.used:0 [number]
xiq.v.trip.consumption:19.2308 [number]
--- Object ----
v.p.trip:8.07781M [string]
xiq.e.trip:0M [string]
xiq.e.trip.energy.recuperated:0kWh [string]
xiq.e.trip.energy.used:0kWh [string]
xiq.v.trip.consumption:309.49Wh/mi [string]
Yeah
- I like
HasValue. I
implemented
IsDefined()
but I will
rename it..
that's a much
clearer name.
Another
thought. How
about if we
did this (but
also with
GetValues() as
well - see the
special values
below)
OvmsMetrics.Value("xiq.v.trip.consumption",
true) -> 17.0582 (Number)
OvmsMetrics.Value("xiq.v.trip.consumption",
false)
-> 17.0582 (String)
OvmsMetrics.Value("xiq.v.trip.consumption",
"mipkwh",
true) -> 3.64264 (Number)
OvmsMetrics.Value("xiq.v.trip.consumption",
"mipkwh",
false) -> 3.64264Mi/kWh (String)
OvmsMetrics.Value("xiq.v.trip.consumption",
"native",
false) -> 17.0582km/kWh (String)
and
OvmsMetrics.Value("xiq.v.trip.consumption",
"imperial",
false) -> 3.64264Mi/kWh (String)
I have
already
implemented
the special
values
'native'
(existing),
'imperial' and
'metric'.
I was
also thinking
that in the
future you
could have
'user'. Where
for each group
of values:
'temperature',
'distance',
'shortdistance',
'power' etc..
you could have
a user
preference. I
probably won't
implement it
now,.but it
could be cool
that any UI
could just ask
for the user
defined units
(rather than
having a
separate
choice).
//.ichael
Or perhaps something more specific?
HasValue()
Mark
Signed
PGP part
That's
basically a
good approach,
but be aware
'IsDefined()'
has an
ambiguous
meaning here,
as with the
API stem
"OvmsMetrics"
it would
naturally be
expected to
mean "is this
metric
defined", not
"does this
metric have a
defined
value".
An undefined
metric
currently can
be derived
from
'Values()'
returning
undefined, but
that's more an
undocumented
side effect
than intended.
Maybe
'GetDefined()'
could be a
better name,
leveraging
this
behaviour,
i.e. returning
'undefined'
for an
actually
undefined
metric, and
'null' for a
defined metric
without a
value.
Regards,
Michael
Am
08.11.22 um
13:46 schrieb
Michael
Geddes:
Ah yes.
Arrays - will
check those.
Yeah, how
about we add a
'IsDefined'
method to
metrics
instead of the
null thing (it
does sound
like it will
upset too many
applecarts).
//.
Michael,
looks all good
to me, once
again nice
find with the
decode
argument.
Adding decode
to the Value()
call was only
for symmetry
IIRC, the main
use was with
GetValues() (
https://docs.openvehicles.com/en/latest/userguide/scripting.html#ovmsmetrics).
Don't forget
to test
arrays, e.g.
"v.t.pressure"
&
"v.t.temp".
Returning null
for an
undefined
metric seems
like a natural
choice, but is
a rather deep
change, as for
consistency
not only the
Duktape
metrics API
but also the
Web UI metrics
API would need
to be changed
accordingly.
Unless you've
got a real use
case that
needs that, we
should be
careful.
Regards,
Michael
Am
07.11.22 um
15:00 schrieb
Michael
Geddes:
I
have figured
out a bunch of
stuff and have
implemented
the following:
(having done
away with
needing
AsFloatUnit)
OvmsMetrics.Value(
{metric} [,
{decode}])
OvmsMetrics.Value(
{metric},
{unit}
[,{decode}])
It turns
out that the
[decode] flag
wasn't working
anyway (since
the function
was being
registered as
only having 1
param)...
This way
it is still
really 1
function.. but
I check it the
second
parameter is a
'boolean', and
if not.. try
the second
form.
OvmsMetrics.AsFloat(
{metric}
[,{unit}] )
and add
the function
Ovms.Metrics.ValueUnit(
{metric}
[,{unit}])
This
prints the
value and the
unit.
Here's a
sample
function and
the output!
This also
shows the
types of the
output.
(function()
{
x =
OvmsMetrics.Value("xiq.v.trip.consumption");
print(
(typeof x) +
": "+ x+"\n"
);
x =
OvmsMetrics.Value("xiq.v.trip.consumption",
false);
print(
(typeof x) +
": "+ x +"\n"
);
x =
OvmsMetrics.Value("xiq.v.trip.consumption","kmpkwh")
print(
(typeof x) +
": "+ x
+"\n");
x =
OvmsMetrics.Value("xiq.v.trip.consumption",
"mipkwh",
false)
print(
(typeof x) +
": "+ x
+"\n");
x =
OvmsMetrics.ValueUnit("xiq.v.trip.consumption")
print(
(typeof x) +
": "+ x
+"\n");
x =
OvmsMetrics.ValueUnit("xiq.v.trip.consumption","mipkwh")
print(
(typeof x) +
": "+ x
+"\n");
x =
OvmsMetrics.AsFloat("xiq.v.trip.consumption")
print(
(typeof x) +
": "+ x
+"\n");
x =
OvmsMetrics.AsFloat("xiq.v.trip.consumption","kmpkwh")
print(
(typeof x) +
": "+ x
+"\n");
})();
number: 17.0582
string: 17.0582
number: 5.86227
string: 3.64264
string: 17.0582kWh/100km
string: 3.64264mi/kWh
number: 17.0582
number: 5.86227
It still
might be an
idea to use
'null' as a
return value
if the metrics
is
!IsDefined() but
that would be
changing the
existing
behaviour
slightly.
//.ichael
I've
worked out
what the
decode flag is
for and how it
works, and I
think how
optional
params work.
I'm
pretty sure I
won't need
the
'AsFloatUnit'
function; the
unit would be
an option to
AsFloat();
I'll know that
soon.
The
'Value'
function is
more
complicated
because of the
optional
decode bool. I
guess I could
add the Unit
to the end of
that.
ValueUnit
could be still
useful then to
provide a
'Value +
Unit'.
Question:
Is there a
reason we
shouldn't be
returning
with duk_push_null
if the
metric
!IsDefined()
in both
AsFloat() and
Value(metric,true) cases?
//.ichael
Right, so
I've
implemented
some stuff
that seems to
work quite
well.
The
commit that
will follow on
from that it
implements the
new Units:
kWh/100km,
km/kWh and
mi/kWh.
This is a
summary of
what I've
implemented
for scripting
- including
showing the
unit codes I
have so far.
I've
considered a
few things:
*
Should some of
the longer
unit codes be
shortened (eg
mi, mins, m,
ft, deg, perc)
* The
unit codes
could be much
more regular
and separated
by dots eg:
watthours
-> w.h
kwhp100km
->
kw.h_100km or
kw.h/100km
miph
-> mi_h or
mi/h (or
should it be
mph).
OVMS#
metric units
km :
km
miles : M
meters : m
feet :
ft
celcius :
°C
fahrenheit :
°F
kpa :
kPa
pa :
Pa
psi :
psi
volts : V
amps : A
amphours
: Ah
kw :
kW
kwh :
kWh
watts : W
watthours
: Wh
seconds :
Sec
minutes :
Min
hours :
Hour
utc :
UTC
degrees : °
kmph :
km/h
miph
: Mph
kmphps
: km/h/s
miphps
: Mph/s
mpss :
m/s²
dbm :
dBm
sq :
sq
percent : %
whpkm :
Wh/km
whpmi
: Wh/mi
kwhp100km :
kWh/100km
kmpkwh
: km/kWh
mipkwh
: mi/kWh
nm :
Nm
OVMS#
metric unit mi
miles :
M