swim.system.warp.WarpDownlinkModem Maven / Gradle / Ivy
The newest version!
// Copyright 2015-2024 Nstream, inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package swim.system.warp;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import swim.concurrent.Cont;
import swim.structure.Value;
import swim.system.CellContext;
import swim.system.DownlinkModel;
import swim.system.DownlinkView;
import swim.system.LinkBinding;
import swim.system.LinkContext;
import swim.system.Metric;
import swim.system.NodeBinding;
import swim.system.Push;
import swim.system.WarpBinding;
import swim.system.WarpContext;
import swim.system.profile.WarpDownlinkProfile;
import swim.uri.Uri;
import swim.warp.CommandMessage;
import swim.warp.Envelope;
import swim.warp.EventMessage;
import swim.warp.LinkRequest;
import swim.warp.LinkedResponse;
import swim.warp.SyncRequest;
import swim.warp.SyncedResponse;
import swim.warp.UnlinkRequest;
import swim.warp.UnlinkedResponse;
public abstract class WarpDownlinkModem extends DownlinkModel implements WarpBinding {
protected final float prio;
protected final float rate;
protected final Value body;
protected WarpContext linkContext;
protected CellContext cellContext;
protected volatile int status;
volatile long execDelta;
volatile long execTime;
volatile int openDelta;
volatile int openCount;
volatile int closeDelta;
volatile int closeCount;
volatile int eventDelta;
volatile int commandDelta;
volatile long eventCount;
volatile long commandCount;
volatile long lastReportTime;
public WarpDownlinkModem(Uri meshUri, Uri hostUri, Uri nodeUri, Uri laneUri,
float prio, float rate, Value body) {
super(meshUri, hostUri, nodeUri, laneUri);
this.prio = prio;
this.rate = rate;
this.body = body;
this.linkContext = null;
this.cellContext = null;
this.status = 0;
this.execDelta = 0L;
this.execTime = 0L;
this.openDelta = 0;
this.openCount = 0;
this.closeDelta = 0;
this.closeCount = 0;
this.eventDelta = 0;
this.commandDelta = 0;
this.eventCount = 0L;
this.commandCount = 0L;
this.lastReportTime = 0L;
}
@Override
public final WarpBinding linkWrapper() {
return this;
}
@Override
public final WarpContext linkContext() {
return this.linkContext;
}
@Override
public void setLinkContext(LinkContext linkContext) {
this.linkContext = (WarpContext) linkContext;
}
@Override
public final CellContext cellContext() {
return this.cellContext;
}
@Override
public void setCellContext(CellContext cellContext) {
this.cellContext = cellContext;
}
@Override
public final float prio() {
return this.prio;
}
@Override
public final float rate() {
return this.rate;
}
@Override
public final Value body() {
return this.body;
}
@Override
public abstract boolean keepLinked();
@Override
public abstract boolean keepSynced();
public void cueDown() {
do {
final int oldStatus = WarpDownlinkModem.STATUS.get(this);
final int newStatus;
if ((oldStatus & WarpDownlinkModem.FEEDING_DOWN) != 0) {
newStatus = oldStatus & ~WarpDownlinkModem.FEEDING_DOWN | WarpDownlinkModem.PULLING_DOWN;
if (WarpDownlinkModem.STATUS.compareAndSet(this, oldStatus, newStatus)) {
this.linkContext.pullDown();
break;
}
} else {
newStatus = oldStatus & ~WarpDownlinkModem.PULLING_DOWN;
if (oldStatus == newStatus || WarpDownlinkModem.STATUS.compareAndSet(this, oldStatus, newStatus)) {
break;
}
}
} while (true);
}
@Override
public void feedDown() {
do {
final int oldStatus = WarpDownlinkModem.STATUS.get(this);
final int newStatus;
if ((oldStatus & WarpDownlinkModem.PULLING_DOWN) == 0) {
newStatus = oldStatus & ~WarpDownlinkModem.FEEDING_DOWN | WarpDownlinkModem.PULLING_DOWN;
if (WarpDownlinkModem.STATUS.compareAndSet(this, oldStatus, newStatus)) {
this.linkContext.pullDown();
break;
}
} else {
newStatus = oldStatus | WarpDownlinkModem.FEEDING_DOWN;
if (oldStatus == newStatus || WarpDownlinkModem.STATUS.compareAndSet(this, oldStatus, newStatus)) {
break;
}
}
} while (true);
}
@SuppressWarnings("unchecked")
@Override
public void pushDown(Push> push) {
final Object message = push.message();
if (message instanceof EventMessage) {
this.pushDownEvent((Push) push);
} else if (message instanceof LinkedResponse) {
this.pushDownLinked((Push) push);
} else if (message instanceof SyncedResponse) {
this.pushDownSynced((Push) push);
} else if (message instanceof UnlinkedResponse) {
this.pushDownUnlinked((Push) push);
} else {
this.pushDownUnknown(push);
}
}
protected void pushDownEvent(Push push) {
try {
this.onEvent(push.message());
} finally {
push.bind();
this.cueDown();
}
}
protected void pushDownLinked(Push push) {
try {
this.didLink(push.message());
} finally {
push.bind();
this.cueDown();
}
}
protected void pushDownSynced(Push push) {
try {
this.didSync(push.message());
} finally {
push.bind();
this.cueDown();
}
}
protected void pushDownUnlinked(Push push) {
try {
this.didUnlink(push.message());
} finally {
push.bind();
// Don't cueDown
}
}
protected void pushDownUnknown(Push> push) {
push.bind();
}
@Override
public void skipDown() {
this.cueDown();
}
protected boolean upQueueIsEmpty() {
return true;
}
protected void queueUp(Value body, Cont cont) {
throw new UnsupportedOperationException();
}
protected Push nextUpQueue() {
return null;
}
protected Push nextUpCue() {
return null;
}
public void pushUp(Value body) {
this.queueUp(body, null);
do {
final int oldStatus = WarpDownlinkModem.STATUS.get(this);
final int newStatus = oldStatus | WarpDownlinkModem.FEEDING_UP;
if (oldStatus != newStatus) {
if (WarpDownlinkModem.STATUS.compareAndSet(this, oldStatus, newStatus)) {
this.linkContext.feedUp();
break;
}
} else {
break;
}
} while (true);
}
public void cueUp() {
do {
final int oldStatus = WarpDownlinkModem.STATUS.get(this);
final int newStatus = oldStatus | (WarpDownlinkModem.FEEDING_UP | WarpDownlinkModem.CUED_UP);
if (oldStatus != newStatus) {
if (WarpDownlinkModem.STATUS.compareAndSet(this, oldStatus, newStatus)) {
if ((oldStatus & WarpDownlinkModem.FEEDING_UP) == 0) {
this.linkContext.feedUp();
}
break;
}
} else {
break;
}
} while (true);
}
protected void feedUp() {
do {
final int oldStatus = WarpDownlinkModem.STATUS.get(this);
if ((oldStatus & WarpDownlinkModem.CUED_UP) != 0 || !this.upQueueIsEmpty()) {
final int newStatus = oldStatus | WarpDownlinkModem.FEEDING_UP;
if (oldStatus != newStatus) {
if (WarpDownlinkModem.STATUS.compareAndSet(this, oldStatus, newStatus)) {
this.linkContext.feedUp();
break;
}
} else {
break;
}
} else {
break;
}
} while (true);
}
@Override
public void pullUp() {
do {
final int oldStatus = WarpDownlinkModem.STATUS.get(this);
final int newStatus;
if ((oldStatus & WarpDownlinkModem.UNLINK) != 0) {
newStatus = oldStatus & ~(WarpDownlinkModem.UNLINK | WarpDownlinkModem.FEEDING_UP);
if (WarpDownlinkModem.STATUS.compareAndSet(this, oldStatus, newStatus)) {
final UnlinkRequest request = this.unlinkRequest();
this.pullUpUnlink(request);
this.pushUp(request);
break;
}
} else if ((oldStatus & WarpDownlinkModem.SYNC) != 0) {
newStatus = oldStatus & ~(WarpDownlinkModem.LINK | WarpDownlinkModem.SYNC | WarpDownlinkModem.FEEDING_UP);
if (WarpDownlinkModem.STATUS.compareAndSet(this, oldStatus, newStatus)) {
final SyncRequest request = this.syncRequest();
this.pullUpSync(request);
this.pushUp(request);
this.feedUp();
break;
}
} else if ((oldStatus & WarpDownlinkModem.LINK) != 0) {
newStatus = oldStatus & ~(WarpDownlinkModem.LINK | WarpDownlinkModem.FEEDING_UP);
if (WarpDownlinkModem.STATUS.compareAndSet(this, oldStatus, newStatus)) {
final LinkRequest request = this.linkRequest();
this.pullUpLink(request);
this.pushUp(request);
this.feedUp();
break;
}
} else {
newStatus = oldStatus & ~(WarpDownlinkModem.CUED_UP | WarpDownlinkModem.FEEDING_UP);
if (oldStatus == newStatus || WarpDownlinkModem.STATUS.compareAndSet(this, oldStatus, newStatus)) {
Push push = this.nextUpQueue();
if (push == null && (oldStatus & WarpDownlinkModem.CUED_UP) != 0) {
push = this.nextUpCue();
}
if (push != null) {
this.pullUpCommand(push.message());
this.linkContext.pushUp(push);
this.feedUp();
} else {
this.linkContext.skipUp();
}
break;
}
}
} while (true);
}
protected void pullUpCommand(CommandMessage message) {
this.onCommand(message);
}
protected void pullUpLink(LinkRequest request) {
this.willLink(request);
}
protected void pullUpSync(SyncRequest request) {
this.willSync(request);
}
protected void pullUpUnlink(UnlinkRequest request) {
this.willUnlink(request);
}
protected void pushUp(Envelope envelope) {
this.linkContext.pushUp(new Push(Uri.empty(), this.hostUri(), this.nodeUri(),
this.laneUri(), this.prio(), null, envelope, null));
}
public void link() {
do {
final int oldStatus = WarpDownlinkModem.STATUS.get(this);
final int newStatus;
if ((oldStatus & (WarpDownlinkModem.LINKED | WarpDownlinkModem.OPENED)) == WarpDownlinkModem.OPENED) {
newStatus = oldStatus | (WarpDownlinkModem.FEEDING_UP | WarpDownlinkModem.LINKING | WarpDownlinkModem.LINK | WarpDownlinkModem.LINKED);
if (WarpDownlinkModem.STATUS.compareAndSet(this, oldStatus, newStatus)) {
if ((oldStatus & WarpDownlinkModem.FEEDING_UP) == 0 && this.linkContext != null) {
this.linkContext.feedUp();
}
break;
}
} else {
break;
}
} while (true);
}
public void sync() {
do {
final int oldStatus = WarpDownlinkModem.STATUS.get(this);
final int newStatus;
if ((oldStatus & (WarpDownlinkModem.LINKED | WarpDownlinkModem.OPENED)) == WarpDownlinkModem.OPENED) {
newStatus = oldStatus | (WarpDownlinkModem.FEEDING_UP | WarpDownlinkModem.SYNCING | WarpDownlinkModem.SYNC | WarpDownlinkModem.LINKING | WarpDownlinkModem.LINK | WarpDownlinkModem.LINKED);
if (WarpDownlinkModem.STATUS.compareAndSet(this, oldStatus, newStatus)) {
if ((oldStatus & WarpDownlinkModem.FEEDING_UP) == 0 && this.linkContext != null) {
this.linkContext.feedUp();
}
break;
}
} else {
break;
}
} while (true);
}
public void unlink() {
do {
final int oldStatus = WarpDownlinkModem.STATUS.get(this);
final int newStatus;
if ((oldStatus & WarpDownlinkModem.LINK) != 0) {
newStatus = oldStatus & ~(WarpDownlinkModem.FEEDING_UP | WarpDownlinkModem.SYNCING | WarpDownlinkModem.SYNC | WarpDownlinkModem.LINKING | WarpDownlinkModem.LINK | WarpDownlinkModem.LINKED);
if (WarpDownlinkModem.STATUS.compareAndSet(this, oldStatus, newStatus)) {
break;
}
} else if ((oldStatus & (WarpDownlinkModem.UNLINKING | WarpDownlinkModem.LINKED)) == WarpDownlinkModem.LINKED) {
newStatus = oldStatus & ~(WarpDownlinkModem.SYNCING | WarpDownlinkModem.SYNC | WarpDownlinkModem.LINKING | WarpDownlinkModem.LINK) | (WarpDownlinkModem.FEEDING_UP | WarpDownlinkModem.UNLINKING | WarpDownlinkModem.UNLINK);
if (WarpDownlinkModem.STATUS.compareAndSet(this, oldStatus, newStatus)) {
if ((oldStatus & WarpDownlinkModem.FEEDING_UP) == 0 && this.linkContext != null) {
this.linkContext.feedUp();
}
break;
}
} else {
break;
}
} while (true);
}
public void command(float prio, Value body, Cont cont) {
this.queueUp(body, cont);
}
public void command(Value body, Cont cont) {
this.queueUp(body, cont);
}
public void command(float prio, Value body) {
this.queueUp(body, null);
}
public void command(Value body) {
this.queueUp(body, null);
}
protected LinkRequest linkRequest() {
return new LinkRequest(this.nodeUri, this.laneUri, this.prio, this.rate, this.body);
}
protected SyncRequest syncRequest() {
return new SyncRequest(this.nodeUri, this.laneUri, this.prio, this.rate, this.body);
}
protected UnlinkRequest unlinkRequest() {
return new UnlinkRequest(this.nodeUri, this.laneUri, this.body);
}
protected void didAddDownlink(View view) {
super.didAddDownlink(view);
WarpDownlinkModem.OPEN_DELTA.incrementAndGet(this);
this.flushMetrics();
}
protected void didRemoveDownlink(View view) {
super.didRemoveDownlink(view);
WarpDownlinkModem.CLOSE_DELTA.incrementAndGet(this);
}
@Override
public void openDown() {
this.didOpen();
this.linkContext.didOpenDown();
if ((WarpDownlinkModem.STATUS.get(this) & WarpDownlinkModem.FEEDING_UP) != 0) {
this.linkContext.feedUp();
}
}
protected void didOpen() {
do {
final int oldStatus = WarpDownlinkModem.STATUS.get(this);
final int newStatus = oldStatus | WarpDownlinkModem.OPENED;
if (oldStatus == newStatus || WarpDownlinkModem.STATUS.compareAndSet(this, oldStatus, newStatus)) {
break;
}
} while (true);
if (this.keepLinked()) {
if (this.keepSynced()) {
this.sync();
} else {
this.link();
}
}
}
@Override
public void closeDown() {
final CellContext cellContext = this.cellContext;
if (cellContext != null) {
cellContext.closeDownlink(this);
}
this.didClose();
this.linkContext.didCloseDown();
}
protected void didClose() {
WarpDownlinkModem.STATUS.set(this, 0);
this.removeDownlinks();
this.flushMetrics();
}
protected void onEvent(EventMessage message) {
EVENT_DELTA.incrementAndGet(this);
this.didUpdateMetrics();
}
protected void onCommand(CommandMessage message) {
COMMAND_DELTA.incrementAndGet(this);
this.didUpdateMetrics();
}
protected void willLink(LinkRequest request) {
// hook
}
protected void didLink(LinkedResponse response) {
do {
final int oldStatus = WarpDownlinkModem.STATUS.get(this);
final int newStatus = oldStatus & ~WarpDownlinkModem.LINKING;
if (oldStatus == newStatus || WarpDownlinkModem.STATUS.compareAndSet(this, oldStatus, newStatus)) {
break;
}
} while (true);
}
protected void willSync(SyncRequest request) {
// hook
}
protected void didSync(SyncedResponse response) {
do {
final int oldStatus = WarpDownlinkModem.STATUS.get(this);
final int newStatus = oldStatus & ~WarpDownlinkModem.SYNCING;
if (oldStatus == newStatus || WarpDownlinkModem.STATUS.compareAndSet(this, oldStatus, newStatus)) {
break;
}
} while (true);
}
protected void willUnlink(UnlinkRequest request) {
// hook
}
protected void didUnlink(UnlinkedResponse response) {
do {
final int oldStatus = WarpDownlinkModem.STATUS.get(this);
final int newStatus = oldStatus & ~(WarpDownlinkModem.PULLING_DOWN | WarpDownlinkModem.UNLINKING | WarpDownlinkModem.UNLINK | WarpDownlinkModem.SYNCING | WarpDownlinkModem.SYNC | WarpDownlinkModem.LINKING | WarpDownlinkModem.LINK | WarpDownlinkModem.LINKED);
if (oldStatus == newStatus || WarpDownlinkModem.STATUS.compareAndSet(this, oldStatus, newStatus)) {
break;
}
} while (true);
}
@Override
public void didConnect() {
if (this.keepLinked()) {
if (this.keepSynced()) {
this.sync();
} else {
this.link();
}
}
super.didConnect();
}
@Override
public void didDisconnect() {
do {
final int oldStatus = WarpDownlinkModem.STATUS.get(this);
final int newStatus = oldStatus & ~(WarpDownlinkModem.FEEDING_UP | WarpDownlinkModem.UNLINKING | WarpDownlinkModem.UNLINK | WarpDownlinkModem.SYNCING | WarpDownlinkModem.SYNC | WarpDownlinkModem.LINKING | WarpDownlinkModem.LINK | WarpDownlinkModem.LINKED);
if (oldStatus == newStatus || WarpDownlinkModem.STATUS.compareAndSet(this, oldStatus, newStatus)) {
break;
}
} while (true);
super.didDisconnect();
}
@Override
public void didCloseUp() {
this.didClose();
super.didCloseUp();
}
@Override
public void didFailUp(Throwable error) {
this.didFail(error);
if (Cont.isNonFatal(error)) {
this.reopen();
}
}
@Override
public void didFail(Throwable error) {
error.printStackTrace();
super.didFail(error);
}
@Override
public void openMetaDownlink(LinkBinding downlink, NodeBinding metaDownlink) {
final CellContext cellContext = this.cellContext;
if (cellContext != null) {
cellContext.openMetaDownlink(downlink, metaDownlink);
}
}
@Override
public void accumulateExecTime(long execDelta) {
WarpDownlinkModem.EXEC_DELTA.addAndGet(this, execDelta);
this.didUpdateMetrics();
}
protected void didUpdateMetrics() {
do {
final long oldReportTime = WarpDownlinkModem.LAST_REPORT_TIME.get(this);
final long newReportTime = System.currentTimeMillis();
final long dt = newReportTime - oldReportTime;
if (dt >= Metric.REPORT_INTERVAL) {
if (WarpDownlinkModem.LAST_REPORT_TIME.compareAndSet(this, oldReportTime, newReportTime)) {
try {
this.reportMetrics(dt);
} catch (Throwable error) {
if (Cont.isNonFatal(error)) {
this.didFail(error);
} else {
throw error;
}
}
break;
}
} else {
break;
}
} while (true);
}
protected void flushMetrics() {
final long newReportTime = System.currentTimeMillis();
final long oldReportTime = LAST_REPORT_TIME.getAndSet(this, newReportTime);
final long dt = newReportTime - oldReportTime;
try {
this.reportMetrics(dt);
} catch (Throwable error) {
if (Cont.isNonFatal(error)) {
this.didFail(error);
} else {
throw error;
}
}
}
protected void reportMetrics(long dt) {
final CellContext cellContext = this.cellContext;
if (cellContext != null) {
final WarpDownlinkProfile profile = this.collectProfile(dt);
cellContext.reportDown(profile);
}
}
protected WarpDownlinkProfile collectProfile(long dt) {
final long execDelta = WarpDownlinkModem.EXEC_DELTA.getAndSet(this, 0L);
final long execRate = (long) Math.ceil((1000.0 * (double) execDelta) / (double) dt);
final long execTime = WarpDownlinkModem.EXEC_TIME.addAndGet(this, execDelta);
final int openDelta = WarpDownlinkModem.OPEN_DELTA.getAndSet(this, 0);
final int openCount = WarpDownlinkModem.OPEN_COUNT.addAndGet(this, openDelta);
final int closeDelta = WarpDownlinkModem.CLOSE_DELTA.getAndSet(this, 0);
final int closeCount = WarpDownlinkModem.CLOSE_COUNT.addAndGet(this, closeDelta);
final int eventDelta = WarpDownlinkModem.EVENT_DELTA.getAndSet(this, 0);
final int eventRate = (int) Math.ceil((1000.0 * (double) eventDelta) / (double) dt);
final long eventCount = WarpDownlinkModem.EVENT_COUNT.addAndGet(this, (long) eventDelta);
final int commandDelta = WarpDownlinkModem.COMMAND_DELTA.getAndSet(this, 0);
final int commandRate = (int) Math.ceil((1000.0 * (double) commandDelta) / (double) dt);
final long commandCount = WarpDownlinkModem.COMMAND_COUNT.addAndGet(this, (long) commandDelta);
return new WarpDownlinkProfile(this.cellAddressDown(), execDelta, execRate, execTime,
openDelta, openCount, closeDelta, closeCount,
eventDelta, eventRate, eventCount,
commandDelta, commandRate, commandCount);
}
static final int OPENED = 1 << 0;
static final int LINKED = 1 << 1;
static final int LINK = 1 << 2;
static final int LINKING = 1 << 3;
static final int SYNC = 1 << 4;
static final int SYNCING = 1 << 5;
static final int UNLINK = 1 << 6;
static final int UNLINKING = 1 << 7;
static final int FEEDING_DOWN = 1 << 8;
static final int PULLING_DOWN = 1 << 9;
static final int CUED_UP = 1 << 10;
static final int FEEDING_UP = 1 << 11;
@SuppressWarnings("unchecked")
static final AtomicIntegerFieldUpdater> STATUS =
AtomicIntegerFieldUpdater.newUpdater((Class>) (Class>) WarpDownlinkModem.class, "status");
@SuppressWarnings("unchecked")
protected static final AtomicLongFieldUpdater> EXEC_DELTA =
AtomicLongFieldUpdater.newUpdater((Class>) (Class>) WarpDownlinkModem.class, "execDelta");
@SuppressWarnings("unchecked")
protected static final AtomicLongFieldUpdater> EXEC_TIME =
AtomicLongFieldUpdater.newUpdater((Class>) (Class>) WarpDownlinkModem.class, "execTime");
@SuppressWarnings("unchecked")
static final AtomicIntegerFieldUpdater> OPEN_DELTA =
AtomicIntegerFieldUpdater.newUpdater((Class>) (Class>) WarpDownlinkModem.class, "openDelta");
@SuppressWarnings("unchecked")
static final AtomicIntegerFieldUpdater> OPEN_COUNT =
AtomicIntegerFieldUpdater.newUpdater((Class>) (Class>) WarpDownlinkModem.class, "openCount");
@SuppressWarnings("unchecked")
static final AtomicIntegerFieldUpdater> CLOSE_DELTA =
AtomicIntegerFieldUpdater.newUpdater((Class>) (Class>) WarpDownlinkModem.class, "closeDelta");
@SuppressWarnings("unchecked")
static final AtomicIntegerFieldUpdater> CLOSE_COUNT =
AtomicIntegerFieldUpdater.newUpdater((Class>) (Class>) WarpDownlinkModem.class, "closeCount");
@SuppressWarnings("unchecked")
static final AtomicIntegerFieldUpdater> EVENT_DELTA =
AtomicIntegerFieldUpdater.newUpdater((Class>) (Class>) WarpDownlinkModem.class, "eventDelta");
@SuppressWarnings("unchecked")
static final AtomicLongFieldUpdater> EVENT_COUNT =
AtomicLongFieldUpdater.newUpdater((Class>) (Class>) WarpDownlinkModem.class, "eventCount");
@SuppressWarnings("unchecked")
static final AtomicIntegerFieldUpdater> COMMAND_DELTA =
AtomicIntegerFieldUpdater.newUpdater((Class>) (Class>) WarpDownlinkModem.class, "commandDelta");
@SuppressWarnings("unchecked")
static final AtomicLongFieldUpdater> COMMAND_COUNT =
AtomicLongFieldUpdater.newUpdater((Class>) (Class>) WarpDownlinkModem.class, "commandCount");
@SuppressWarnings("unchecked")
static final AtomicLongFieldUpdater> LAST_REPORT_TIME =
AtomicLongFieldUpdater.newUpdater((Class>) (Class>) WarpDownlinkModem.class, "lastReportTime");
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy