From a3c5363abb1257f49af91938300be78bc78ded94 Mon Sep 17 00:00:00 2001
From: Mandar Chitre <mandar@nus.edu.sg>
Date: Mon, 29 Jan 2024 00:30:18 +0800
Subject: [PATCH] refactor: improve message implementation

---
 src/const.jl |   2 +-
 src/msg.jl   | 664 +++++++++++++++++++++++++--------------------------
 2 files changed, 332 insertions(+), 334 deletions(-)

diff --git a/src/const.jl b/src/const.jl
index d4be014..cae867c 100644
--- a/src/const.jl
+++ b/src/const.jl
@@ -6,7 +6,7 @@ module Services
 end
 
 "Shell command execution request message."
-@msg "org.arl.fjage.shell.ShellExecReq" struct ShellExecReq
+@message "org.arl.fjage.shell.ShellExecReq" struct ShellExecReq
   cmd::Union{String,Nothing} = nothing
   script::Union{String,Nothing} = nothing
   args::Vector{String} = String[]
diff --git a/src/msg.jl b/src/msg.jl
index 01bdd91..1954d5d 100644
--- a/src/msg.jl
+++ b/src/msg.jl
@@ -1,4 +1,4 @@
-export Performative, Message, GenericMessage, MessageClass, AbstractMessageClass, clone, ParameterReq, ParameterRsp, set!
+export Performative, Message, GenericMessage, @message, classname, clone, ParameterReq, ParameterRsp, set!
 
 # global variables
 const _messageclasses = Dict{String,DataType}()
@@ -22,132 +22,84 @@ end
 "Base class for messages transmitted by one agent to another."
 abstract type Message end
 
-function clazz end
+"""
+    classname(msg::Message)
+
+Return the fully qualified class name of a message.
+"""
+function classname end
 
-function _msg(clazz, perf, sdef)
+function _message(classname, perf, sdef)
   if @capture(sdef, struct T_ <: P_ fields__ end)
-    push!(fields, :(msgID::String = string(Fjage.uuid4())))
-    push!(fields, :(perf::Symbol = $perf))
+    if T == P
+      T = Symbol("_" * string(T))
+      extra = :( $(P)(; kwargs...) = $(T)(; kwargs...) )
+    else
+      extra = :()
+    end
+    push!(fields, :(messageID::String = string(Fjage.uuid4())))
+    push!(fields, :(performative::Symbol = $perf))
     push!(fields, :(sender::Union{AgentID,Nothing} = nothing))
     push!(fields, :(recipient::Union{AgentID,Nothing} = nothing))
     push!(fields, :(inReplyTo::Union{String,Nothing} = nothing))
     push!(fields, :(sentAt::Int64 = 0))
     quote
       Base.@kwdef mutable struct $(T) <: $(P); $(fields...); end
-      Fjage.clazz(::Type{$(T)}) = $(clazz)
-      Fjage.clazz(::$(T)) = $(clazz)
-      Fjage._messageclasses[$(clazz)] = $(T)
+      Fjage.classname(::Type{$(T)}) = $(classname)
+      Fjage.classname(::$(T)) = $(classname)
+      Fjage._messageclasses[$(classname)] = $(T)
+      $extra
     end |> esc
   elseif @capture(sdef, struct T_ fields__ end)
-    push!(fields, :(msgID::String = string(Fjage.uuid4())))
-    push!(fields, :(perf::Symbol = $perf))
+    push!(fields, :(messageID::String = string(Fjage.uuid4())))
+    push!(fields, :(performative::Symbol = $perf))
     push!(fields, :(sender::Union{AgentID,Nothing} = nothing))
     push!(fields, :(recipient::Union{AgentID,Nothing} = nothing))
     push!(fields, :(inReplyTo::Union{String,Nothing} = nothing))
     push!(fields, :(sentAt::Int64 = 0))
     quote
       Base.@kwdef mutable struct $(T) <: Fjage.Message; $(fields...); end
-      Fjage.clazz(::Type{$(T)}) = $(clazz)
-      Fjage.clazz(::$(T)) = $(clazz)
-      Fjage._messageclasses[$(clazz)] = $(T)
+      Fjage.classname(::Type{$(T)}) = $(classname)
+      Fjage.classname(::$(T)) = $(classname)
+      Fjage._messageclasses[$(classname)] = $(T)
     end |> esc
   else
     @error "Bad message definition"
   end
 end
 
-macro msg(clazz, perf, sdef)
+"""
+    @message classname [performative] struct mtype [<: supertype]
+      fields...
+    end
+
+Create a message class from a fully qualified class name. If a performative is not
+specified, it is guessed based on the class name. For class names ending with "Req",
+the performative is assumed to be REQUEST, and for all other messages, INFORM.
+
+# Examples
+
+```julia-repl
+julia> @message "org.arl.fjage.shell.MyShellExecReq" struct MyShellExecReq
+         cmd::String
+       end
+julia> req = MyShellExecReq(cmd="ps")
+MyShellExecReq:REQUEST[cmd:"ps"]
+```
+"""
+macro message(classname, perf, sdef)
   perf = QuoteNode(eval(perf))
-  _msg(clazz, perf, sdef)
+  _message(classname, perf, sdef)
 end
 
-macro msg(clazz, sdef)
-  perf = endswith(clazz, "Req") ? :(:REQUEST) : :(:INFORM)
-  _msg(clazz, perf, sdef)
+macro message(classname, sdef)
+  perf = endswith(classname, "Req") ? :(:REQUEST) : :(:INFORM)
+  _message(classname, perf, sdef)
 end
 
-
-# """
-#     mtype = MessageClass(context, clazz[, superclass[, performative]])
-
-# Create a message class from a fully qualified class name. If a performative is not
-# specified, it is guessed based on the class name. For class names ending with "Req",
-# the performative is assumed to be REQUEST, and for all other messages, INFORM.
-
-# # Examples
-
-# ```julia-repl
-# julia> MyShellExecReq = MessageClass(@__MODULE__, "org.arl.fjage.shell.ShellExecReq");
-# julia> req = MyShellExecReq(cmd="ps")
-# ShellExecReq: REQUEST [cmd:"ps"]
-# ```
-# """
-# function MessageClass(context, clazz::String, superclass=nothing, performative=nothing)
-#   sname = replace(string(clazz), "." => "_")
-#   tname = sname
-#   if performative === nothing
-#     performative = match(r"Req$",string(clazz))==nothing ? Performative.INFORM : Performative.REQUEST
-#   end
-#   if superclass === nothing
-#     superclass = "$(@__MODULE__).Message"
-#   else
-#     scname = string(superclass)
-#     ndx = findlast(isequal('.'), scname)
-#     if ndx !== nothing
-#       scname = scname[ndx+1:end]
-#     end
-#     if scname == tname
-#       tname = "$(scname)_"
-#     end
-#   end
-#   expr = Expr(:toplevel)
-#   expr.args = [Meta.parse("""
-#       struct $(tname) <: $(superclass)
-#         clazz::String
-#         data::Dict{String,Any}
-#         $(tname)(c::String, d::Dict{String,Any}) = new(c, d)
-#       end
-#     """),
-#     Meta.parse("""
-#       function $(sname)(d::Dict{String, Any})
-#         get!(d, "msgID", string($(@__MODULE__).uuid4()))
-#         get!(d, "perf", "$performative")
-#         return $(tname)("$(clazz)", d)
-#       end
-#     """),
-#     Meta.parse("""
-#       function $(sname)(; kwargs...)
-#         dict = Dict{String,Any}(
-#           "msgID" => string($(@__MODULE__).uuid4()),
-#           "perf" => "$(performative)"
-#         )
-#         for k in keys(kwargs)
-#           dict[string(k)] = kwargs[k]
-#         end
-#         return $(tname)("$(clazz)", dict)
-#       end
-#     """),
-#     Meta.parse("""
-#       $(@__MODULE__)._messageclasses["$(clazz)"] = $(tname)
-#     """)]
-#   if sname != tname
-#     push!(expr.args, Meta.parse("$(tname)(; kwargs...) = $(sname)(; kwargs...)"))
-#   end
-#   return context.eval(expr)
-# end
-
-# function AbstractMessageClass(context, clazz::String, performative=nothing)
-#   sname = replace(string(clazz), "." => "_")
-#   expr = Expr(:toplevel)
-#   expr.args = [Meta.parse("abstract type $sname <: $(@__MODULE__).Message end"), Meta.parse("$sname")]
-#   rv = context.eval(expr)
-#   MessageClass(context, clazz, rv, performative)
-#   return rv
-# end
-
 function clone(original::Message)
   cloned = deepcopy(original)
-  cloned.msgID = string(uuid4())
+  cloned.messageID = string(uuid4())
   return cloned
 end
 
@@ -156,7 +108,7 @@ end
     registermessages(messageclasses)
 
 Register message classes with Fjage. Usually message classes are automatically registered on
-creation with `@msg`. However, when developing packages, if `@msg` is used at the module level,
+creation with `@message`. However, when developing packages, if `@message` is used at the module level,
 the types may be precompiled and the code to register the classes may not get executed at runtime.
 In such cases, you may need to explicitly call `registermessages()` in the `__init()__` function
 for the module.
@@ -164,14 +116,14 @@ for the module.
 function registermessages(msg=subtypes(Message))
   for T ∈ msg
     endswith(string(T), '_') && continue
-    s = clazz(T)
+    s = classname(T)
     _messageclasses[s] = T
     registermessages(subtypes(T))
   end
 end
 
-function _messageclass_lookup(clazz::String)
-  haskey(_messageclasses, clazz) && return _messageclasses[clazz]
+function _messageclass_lookup(classname::String)
+  haskey(_messageclasses, classname) && return _messageclasses[classname]
   Message
 end
 
@@ -181,7 +133,7 @@ function _matches(filt, msg)
   if typeof(filt) == DataType
     return typeof(msg) <: filt
   elseif typeof(filt) <: Message
-    return msg.inReplyTo == filt.msgID
+    return msg.inReplyTo == filt.messageID
   elseif typeof(filt) <: Function
     return filt(msg)
   end
@@ -189,22 +141,13 @@ function _matches(filt, msg)
 end
 
 # adds notation message.field
+
 function Base.getproperty(s::Message, p::Symbol)
-  if p == :performative
-    p = :perf
-  elseif p == :messageID
-    p = :msgID
-  end
   hasfield(typeof(s), p) || return nothing
   getfield(s, p)
 end
 
 function Base.setproperty!(s::Message, p::Symbol, v)
-  if p == :performative
-    p = :perf
-  elseif p == :messageID
-    p = :msgID
-  end
   hasfield(typeof(s), p) || throw(ArgumentError("$(typeof(s)) has no property called $p"))
   setfield!(s, p, v)
 end
@@ -235,250 +178,305 @@ function Base.iterate(s::Message, state)
   (state[1][1] => state[2][1], (state[1][2:end], state[2][2:end]))
 end
 
-# # pretty prints arrays without type names
-# function _repr(x)
-#   x = repr(x)
-#   m = match(r"[A-Za-z0-9]+(\[.+\])", x)
-#   m !== nothing && (x = m[1])
-#   x
-# end
+# pretty prints arrays without type names
+function _repr(x)
+  x = repr(x)
+  m = match(r"[A-Za-z0-9]+(\[.+\])", x)
+  m !== nothing && (x = m[1])
+  m = match(r"^\w+(\[.*)$", x)
+  m !== nothing && (x = m[1])
+  x
+end
 
 # pretty printing of messages
-# function Base.show(io::IO, msg::Message)
-#   ndx = findlast(".", msg.__clazz__)
-#   s = ndx === nothing ? msg.__clazz__ : msg.__clazz__[ndx[1]+1:end]
-#   p = ""
-#   data_suffix = ""
-#   signal_suffix = ""
-#   suffix = ""
-#   data = msg.__data__
-#   for k in keys(data)
-#     x = data[k]
-#     if k == "perf"
-#       s *= ": " * x
-#     elseif k == "data"
-#       if typeof(x) <: Array
-#         data_suffix *= "($(length(x)) bytes)"
-#       else
-#         p *= " $k:" * _repr(data[k])
-#       end
-#     elseif k == "signal"
-#       if typeof(x) <: Array
-#         signal_suffix *= "($(length(x)) samples)"
-#       else
-#         p *= " $k:" * _repr(data[k])
-#       end
-#     elseif k != "sender" && k != "recipient" && k != "msgID" && k != "inReplyTo" && k != "sentAt"
-#       if typeof(x) <: Number || typeof(x) == String || typeof(x) <: Array || typeof(x) == Bool
-#         p *= " $k:" * _repr(x)
-#       else
-#         suffix = "..."
-#       end
-#     end
-#   end
-#   length(suffix) > 0 && (p *= " " * suffix)
-#   length(signal_suffix) > 0 && (p *= " " * signal_suffix)
-#   length(data_suffix) > 0 && (p *= " " * data_suffix)
-#   p = strip(p)
-#   length(p) > 0 && (s *= " [$p]")
-#   if msg.__clazz__ == "org.arl.fjage.GenericMessage"
-#     m = match(r"^GenericMessage: (.*)$", s)
-#     m === nothing || (s = m[1])
-#   end
-#   print(io, s)
-# end
+function Base.show(io::IO, msg::Message)
+  s = classname(msg)
+  ndx = findlast(".", s)
+  ndx === nothing || (s = s[ndx[1]+1:end])
+  p = ""
+  data_suffix = ""
+  signal_suffix = ""
+  suffix = ""
+  for k in keys(msg)
+    x = msg[k]
+    if k == :performative
+      s *= ":" * string(x)
+    elseif k == :data
+      if typeof(x) <: Array
+        data_suffix *= "($(length(x)) bytes)"
+      else
+        p *= " $k:" * _repr(msg[k])
+      end
+    elseif k == :signal
+      if typeof(x) <: Array
+        signal_suffix *= "($(length(x)) samples)"
+      else
+        p *= " $k:" * _repr(msg[k])
+      end
+    elseif k != :sender && k != :recipient && k != :messageID && k != :inReplyTo && k != :sentAt
+      if typeof(x) <: Number || typeof(x) == String || typeof(x) <: Array || typeof(x) == Bool
+        p *= " $k:" * _repr(x)
+      elseif x !== nothing && x !== missing
+        suffix = "..."
+      end
+    end
+  end
+  length(suffix) > 0 && (p *= " " * suffix)
+  length(signal_suffix) > 0 && (p *= " " * signal_suffix)
+  length(data_suffix) > 0 && (p *= " " * data_suffix)
+  p = strip(p)
+  length(p) > 0 && (s *= "[$p]")
+  print(io, s)
+end
 
 # concrete message without data
-@msg "org.arl.fjage.Message" struct _Message end
+@message "org.arl.fjage.Message" struct _Message end
 
 "Generic message type that can carry arbitrary name-value pairs as data."
 abstract type GenericMessage <: Message end
 
 # concrete generic message
-@msg "org.arl.fjage.GenericMessage" struct _GenericMessage <: GenericMessage
-  data::Dict{String,Any} = Dict{String,Any}()
+@message "org.arl.fjage.GenericMessage" struct _GenericMessage <: GenericMessage
+  __data__::Dict{Symbol,Any} = Dict{Symbol,Any}()
+end
+
+GenericMessage(perf::Symbol=Performative.INFORM; kwargs...) = _GenericMessage(performative=perf, kwargs...)
+GenericMessage(inreplyto::Message, perf::Symbol=Performative.INFORM; kwargs...) = _GenericMessage(performative=perf, inReplyTo=inreplyto.messageID, recipient=inreplyto.sender, kwargs...)
+
+# adds notation message.field
+
+function Base.getproperty(s::GenericMessage, p::Symbol)
+  hasfield(typeof(s), p) && return getfield(s, p)
+  haskey(s.__data__, p) && return s.__data__[p]
+  nothing
+end
+
+function Base.setproperty!(s::GenericMessage, p::Symbol, v)
+  if hasfield(typeof(s), p)
+    setfield!(s, p, v)
+  else
+    s.__data__[p] = v
+  end
 end
 
-GenericMessage(perf::Symbol=Performative.INFORM; kwargs...) = _GenericMessage(perf=perf, kwargs...)
-GenericMessage(inreplyto::Message, perf::Symbol=Performative.INFORM; kwargs...) = _GenericMessage(perf=perf, inReplyTo=inreplyto.msgID, recipient=inreplyto.sender, kwargs...)
+# dictionary interface for GenericMessages
+
+function Base.get(s::GenericMessage, p::Symbol, default)
+  hasfield(typeof(s), p) && return getfield(s, p)
+  haskey(s.__data__, p) && return s.__data__[p]
+  default
+end
+
+function Base.keys(s::GenericMessage)
+  k = Set(keys(s.__data__))
+  for f ∈ fieldnames(typeof(s))
+    f == :__data__ && continue
+    push!(k, f)
+  end
+  k
+end
+
+function Base.values(s::GenericMessage)
+  v = Any[]
+  for k ∈ keys(s)
+    push!(v, s[k])
+  end
+  v
+end
+
+Base.length(s::GenericMessage) = fieldcount(typeof(s)) - 1 + length(s.__data__)
+
+function Base.iterate(s::GenericMessage)
+  f = keys(s)
+  v = values(s)
+  (f[1] => v[1], (f[2:end], v[2:end]))
+end
+
+"""
+    msg = Message([perf])
+    msg = Message(inreplyto[, perf])
+
+Create a message with just a performative (`perf`) and no data. If the performative
+is not specified, it defaults to INFORM. If the inreplyto is specified, the message
+`inReplyTo` and `recipient` fields are set accordingly.
+"""
+Message(perf::Symbol=Performative.INFORM) = _Message(performative=perf)
+Message(inreplyto::Message, perf::Symbol=Performative.INFORM) = _Message(performative=perf, inReplyTo=inreplyto.messageID, recipient=inreplyto.sender)
 
 "Parameter request message."
-@msg "org.arl.fjage.param.ParameterReq" struct ParameterReq
+@message "org.arl.fjage.param.ParameterReq" struct ParameterReq
   index::Int = -1
   param::Union{String,Nothing} = nothing
   value::Union{Any,Nothing} = nothing
-  requests::Union{Dict{String,Any},Nothing} = nothing
+  requests::Union{Vector{Dict{String,Any}},Nothing} = nothing
 end
 
 "Parameter response message."
-@msg "org.arl.fjage.param.ParameterRsp" struct ParameterRsp
+@message "org.arl.fjage.param.ParameterRsp" struct ParameterRsp
   index::Int = -1
   param::Union{String,Nothing} = nothing
   value::Union{Any,Nothing} = nothing
-  values::Union{Dict{String,Any},Nothing} = nothing
+  values::Union{Vector{Dict{String,Any}},Nothing} = nothing
+end
+
+# convenience methods and pretty printing for parameters
+
+function ParameterReq(vals...; index=-1)
+  req = ParameterReq(index=index)
+  qlist = Pair{String,Any}[]
+  for v ∈ vals
+    if v isa String
+      push!(qlist, Pair{String,Any}(v, nothing))
+    elseif v isa Symbol
+      push!(qlist, Pair{String,Any}(string(v), nothing))
+    elseif v isa Pair
+      push!(qlist, Pair{String,Any}(string(v[1]), v[2]))
+    end
+  end
+  if length(qlist) > 0
+    q = popfirst!(qlist)
+    req.param = q[1]
+    req.value = q[2]
+    if length(qlist) > 0
+      req.requests = Dict{String,Any}[]
+      for q ∈ qlist
+        push!(req.requests, Dict{String,Any}("param" => q[1], "value" => q[2]))
+      end
+    end
+  end
+  req
 end
 
 """
-    msg = Message([perf])
-    msg = Message(inreplyto[, perf])
+    get!(p::ParameterReq, param)
 
-Create a message with just a performative (`perf`) and no data. If the performative
-is not specified, it defaults to INFORM. If the inreplyto is specified, the message
-`inReplyTo` and `recipient` fields are set accordingly.
+Request parameter `param` to be fetched.
+
+# Examples
+
+```julia-repl
+julia> p = ParameterReq(index=1)
+ParameterReq[index=1]
+julia> get!(p, "modulation")
+ParameterReq[index=1 modulation=?]
+julia> get!(p, "fec")
+ParameterReq[index=1 modulation=? ...]
+```
+"""
+function Base.get!(p::ParameterReq, param)
+  param = string(param)
+  if p.param === nothing
+    p.param = param
+  else
+    p.requests === nothing && (p.requests = Dict{String,Any}[])
+    push!(p.requests, Dict{String,Any}("param" => param))
+  end
+  p
+end
+
+"""
+    set!(p::ParameterReq, param, value)
+
+Request parameter `param` to be set to `value`.
+
+# Examples
+
+```julia-repl
+julia> p = ParameterReq(index=1)
+ParameterReq[index=1]
+julia> set!(p, "modulation", "ofdm")
+ParameterReq[index=1 modulation=ofdm]
+julia> set!(p, "fec", 1)
+ParameterReq[index=1 modulation=ofdm ...]
+```
 """
-Message(perf::Symbol=Performative.INFORM) = _Message(perf=perf)
-Message(inreplyto::Message, perf::Symbol=Performative.INFORM) = _Message(perf=perf, inReplyTo=inreplyto.msgID, recipient=inreplyto.sender)
-
-# # convenience methods and pretty printing for parameters
-
-# function org_arl_fjage_param_ParameterReq(vals...; index=-1)
-#   req = ParameterReq(index=index)
-#   qlist = Pair{String,Any}[]
-#   for v ∈ vals
-#     if v isa String
-#       push!(qlist, Pair{String,Any}(v, nothing))
-#     elseif v isa Symbol
-#       push!(qlist, Pair{String,Any}(string(v), nothing))
-#     elseif v isa Pair
-#       push!(qlist, Pair{String,Any}(string(v[1]), v[2]))
-#     end
-#   end
-#   if length(qlist) > 0
-#     q = popfirst!(qlist)
-#     req.param = q[1]
-#     req.value = q[2]
-#     if length(qlist) > 0
-#       req.requests = Dict{String,Any}[]
-#       for q ∈ qlist
-#         push!(req.requests, Dict{String,Any}("param" => q[1], "value" => q[2]))
-#       end
-#     end
-#   end
-#   req
-# end
-
-# """
-#     get!(p::ParameterReq, param)
-
-# Request parameter `param` to be fetched.
-
-# # Examples
-
-# ```julia-repl
-# julia> p = ParameterReq(index=1)
-# ParameterReq[index=1]
-# julia> get!(p, "modulation")
-# ParameterReq[index=1 modulation=?]
-# julia> get!(p, "fec")
-# ParameterReq[index=1 modulation=? ...]
-# ```
-# """
-# function Base.get!(p::ParameterReq, param)
-#   if p.param === nothing
-#     p.param = param
-#   else
-#     p.requests === nothing && (p.requests = Dict{String,Any}[])
-#     push!(p.requests, Dict{String,Any}("param" => param))
-#   end
-#   p
-# end
-
-# """
-#     set!(p::ParameterReq, param, value)
-
-# Request parameter `param` to be set to `value`.
-
-# # Examples
-
-# ```julia-repl
-# julia> p = ParameterReq(index=1)
-# ParameterReq[index=1]
-# julia> set!(p, "modulation", "ofdm")
-# ParameterReq[index=1 modulation=ofdm]
-# julia> set!(p, "fec", 1)
-# ParameterReq[index=1 modulation=ofdm ...]
-# ```
-# """
-# function set!(p::ParameterReq, param, value)
-#   if p.param === nothing
-#     p.param = param
-#     p.value = value
-#   else
-#     p.requests === nothing && (p.requests = Dict{String,Any}[])
-#     push!(p.requests, Dict{String,Any}("param" => param, "value" => value))
-#   end
-#   p
-# end
-
-# """
-#     get(p::ParameterRsp, param)
-
-# Extract parameter `param` from a parameter response message.
-# """
-# function Base.get(p::ParameterRsp, key)
-#   skey = string(key)
-#   dskey = "." * skey
-#   (!isnothing(p.param) && (p.param == skey || endswith(p.param, dskey))) && return p.value
-#   vals = p.values
-#   if vals !== nothing
-#     for (k, v) ∈ vals
-#       (k == skey || endswith(k, dskey)) && return v
-#     end
-#   end
-#   nothing
-# end
-
-# function Base.show(io::IO, p::ParameterReq)
-#   print(io, "ParameterReq[")
-#   p.index !== nothing && p.index ≥ 0 && print(io, "index=", p.index, ' ')
-#   p.param !== nothing && print(io, p.param, '=', (p.value === nothing ? "?" : string(p.value)))
-#   p.requests === nothing || print(io, " ...")
-#   print(io, ']')
-# end
-
-# function Base.show(io::IO, p::ParameterRsp)
-#   print(io, "ParameterRsp[")
-#   p.index !== nothing && p.index ≥ 0 && print(io, "index=", p.index, ' ')
-#   p.param !== nothing && print(io, p.param, '=', p.value)
-#   p.values === nothing || print(io, " ...")
-#   print(io, ']')
-# end
-
-# function Base.println(io::IO, p::ParameterRsp)
-#   plist = Pair{String,Any}[]
-#   if p.param !== nothing
-#     x = p.param
-#     occursin(".", x) || (x = "." * x)
-#     push!(plist, x => p.value)
-#     vs = p.values
-#     if vs !== nothing
-#       for v ∈ vs
-#         x = v[1]
-#         occursin(".", x) || (x = "." * x)
-#         push!(plist, x => v[2])
-#       end
-#     end
-#   end
-#   sort!(plist; by=(x -> x[1]))
-#   let n = findfirst(x -> x[1] == ".title", plist)
-#     n === nothing || println(io, "« ", plist[n][2], " »\n")
-#   end
-#   let n = findfirst(x -> x[1] == ".description", plist)
-#     n === nothing || plist[n][2] == "" || println(io, plist[n][2], "\n")
-#   end
-#   prefix = ""
-#   ro = p.readonly === nothing ? String[] : p.readonly
-#   for (k, v) ∈ plist
-#     k === ".type" && continue
-#     k === ".title" && continue
-#     k === ".description" && continue
-#     ks = split(k, '.')
-#     cprefix = join(ks[1:end-1], '.')
-#     if cprefix != prefix
-#       prefix != "" && println(io)
-#       prefix = cprefix
-#       println(io, '[', cprefix, ']')
-#     end
-#     println(io, "  ", ks[end], k ∈ ro ? " ⤇ " : " = ", v)
-#   end
-# end
+function set!(p::ParameterReq, param, value)
+  param = string(param)
+  if p.param === nothing
+    p.param = param
+    p.value = value
+  else
+    p.requests === nothing && (p.requests = Dict{String,Any}[])
+    push!(p.requests, Dict{String,Any}("param" => param, "value" => value))
+  end
+  p
+end
+
+"""
+    get(p::ParameterRsp, param)
+
+Extract parameter `param` from a parameter response message.
+"""
+function Base.get(p::ParameterRsp, key)
+  skey = string(key)
+  dskey = "." * skey
+  (!isnothing(p.param) && (p.param == skey || endswith(p.param, dskey))) && return p.value
+  vals = p.values
+  if vals !== nothing
+    for (k, v) ∈ vals
+      (k == skey || endswith(k, dskey)) && return v
+    end
+  end
+  nothing
+end
+
+function Base.show(io::IO, p::ParameterReq)
+  print(io, "ParameterReq[")
+  if p.index !== nothing && p.index ≥ 0
+    print(io, "index=", p.index)
+    p.param === nothing || print(io, ' ')
+  end
+  p.param === nothing || print(io, p.param, '=', (p.value === nothing ? "?" : string(p.value)))
+  p.requests === nothing || print(io, " ...")
+  print(io, ']')
+end
+
+function Base.show(io::IO, p::ParameterRsp)
+  print(io, "ParameterRsp[")
+  if p.index !== nothing && p.index ≥ 0
+    print(io, "index=", p.index)
+    p.param === nothing || print(io, ' ')
+  end
+  p.param === nothing || print(io, p.param, '=', p.value)
+  p.values === nothing || print(io, " ...")
+  print(io, ']')
+end
+
+function Base.println(io::IO, p::ParameterRsp)
+  plist = Pair{String,Any}[]
+  if p.param !== nothing
+    x = p.param
+    occursin(".", x) || (x = "." * x)
+    push!(plist, x => p.value)
+    vs = p.values
+    if vs !== nothing
+      for v ∈ vs
+        x = v[1]
+        occursin(".", x) || (x = "." * x)
+        push!(plist, x => v[2])
+      end
+    end
+  end
+  sort!(plist; by=(x -> x[1]))
+  let n = findfirst(x -> x[1] == ".title", plist)
+    n === nothing || println(io, "« ", plist[n][2], " »\n")
+  end
+  let n = findfirst(x -> x[1] == ".description", plist)
+    n === nothing || plist[n][2] == "" || println(io, plist[n][2], "\n")
+  end
+  prefix = ""
+  ro = p.readonly === nothing ? String[] : p.readonly
+  for (k, v) ∈ plist
+    k === ".type" && continue
+    k === ".title" && continue
+    k === ".description" && continue
+    ks = split(k, '.')
+    cprefix = join(ks[1:end-1], '.')
+    if cprefix != prefix
+      prefix != "" && println(io)
+      prefix = cprefix
+      println(io, '[', cprefix, ']')
+    end
+    println(io, "  ", ks[end], k ∈ ro ? " ⤇ " : " = ", v)
+  end
+end