Merge branch 'liamcottle:master' into docker-image
This commit is contained in:
@@ -41,6 +41,7 @@ A simple mesh network communications app powered by the [Reticulum Network Stack
|
|||||||
- Supports auto resending undelivered messages when an announce is received from the recipient.
|
- Supports auto resending undelivered messages when an announce is received from the recipient.
|
||||||
- Supports sending messages to and syncing messages from [LXMF Propagation Nodes](https://github.com/markqvist/lxmf?tab=readme-ov-file#propagation-nodes).
|
- Supports sending messages to and syncing messages from [LXMF Propagation Nodes](https://github.com/markqvist/lxmf?tab=readme-ov-file#propagation-nodes).
|
||||||
- Supports running a local LXMF Propagation Node so other users can use your device for message storage and retrieval.
|
- Supports running a local LXMF Propagation Node so other users can use your device for message storage and retrieval.
|
||||||
|
- Support for browsing pages, and downloading files hosted on Nomad Network Nodes.
|
||||||
|
|
||||||
## Beta Features
|
## Beta Features
|
||||||
|
|
||||||
@@ -49,9 +50,6 @@ A simple mesh network communications app powered by the [Reticulum Network Stack
|
|||||||
- Using a microphone requires using the web ui over localhost or https, due to [AudioWorklet](https://developer.mozilla.org/en-US/docs/Web/API/AudioWorklet) secure context.
|
- Using a microphone requires using the web ui over localhost or https, due to [AudioWorklet](https://developer.mozilla.org/en-US/docs/Web/API/AudioWorklet) secure context.
|
||||||
- I have tested two-way audio calls over LoRa with a single hop. It works well when a [reasonable bitrate](https://unsigned.io/understanding-lora-parameters/) is configured on the RNode.
|
- I have tested two-way audio calls over LoRa with a single hop. It works well when a [reasonable bitrate](https://unsigned.io/understanding-lora-parameters/) is configured on the RNode.
|
||||||
- Some browsers such as FireFox don't work as expected. Try using a Chromium based browser if running via the command line.
|
- Some browsers such as FireFox don't work as expected. Try using a Chromium based browser if running via the command line.
|
||||||
- Support for browsing pages, and downloading files hosted on Nomad Network Nodes.
|
|
||||||
|
|
||||||
> NOTE: micron format parsing is still in development, some pages may not render or work correctly at all.
|
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
@@ -374,7 +372,6 @@ I build the vite app everytime without hot reload, since MeshChat expects everyt
|
|||||||
- [ ] button to forget announces
|
- [ ] button to forget announces
|
||||||
- [ ] optimise ui to work nicely on a mobile device, such as Android/iOS
|
- [ ] optimise ui to work nicely on a mobile device, such as Android/iOS
|
||||||
- [ ] will probably write a new app for mobile devices once [microReticulum](https://github.com/attermann/microReticulum) supports Links
|
- [ ] will probably write a new app for mobile devices once [microReticulum](https://github.com/attermann/microReticulum) supports Links
|
||||||
- [ ] support for micron input fields, to allow interacting with pages like Retipedia
|
|
||||||
- [ ] support for managing Reticulum interfaces via the web ui
|
- [ ] support for managing Reticulum interfaces via the web ui
|
||||||
- [x] AutoInterface
|
- [x] AutoInterface
|
||||||
- [x] RNodeInterface
|
- [x] RNodeInterface
|
||||||
@@ -395,7 +392,7 @@ I build the vite app everytime without hot reload, since MeshChat expects everyt
|
|||||||
**LXMF Router**
|
**LXMF Router**
|
||||||
|
|
||||||
- By default, the LXMF router rejects inbound messages larger than 1mb.
|
- By default, the LXMF router rejects inbound messages larger than 1mb.
|
||||||
- LXMF clients are likely to have [this default limit](https://github.com/markqvist/LXMF/blob/master/LXMF/LXMRouter.py#L35), and your messages will [fail to send](https://github.com/markqvist/LXMF/blob/master/LXMF/LXMRouter.py#L1026).
|
- LXMF clients are likely to have [this default limit](https://github.com/markqvist/LXMF/blob/c426c93cc5d63a3dae18ad2264b1299a7ad9e46c/LXMF/LXMRouter.py#L38), and your messages will [fail to send](https://github.com/markqvist/LXMF/blob/c426c93cc5d63a3dae18ad2264b1299a7ad9e46c/LXMF/LXMRouter.py#L1428).
|
||||||
- MeshChat has increased the receive limit to 10mb to allow for larger attachments.
|
- MeshChat has increased the receive limit to 10mb to allow for larger attachments.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
13
database.py
13
database.py
@@ -3,7 +3,7 @@ from datetime import datetime, timezone
|
|||||||
from peewee import *
|
from peewee import *
|
||||||
from playhouse.migrate import migrate as migrate_database, SqliteMigrator
|
from playhouse.migrate import migrate as migrate_database, SqliteMigrator
|
||||||
|
|
||||||
latest_version = 4 # increment each time new database migrations are added
|
latest_version = 5 # increment each time new database migrations are added
|
||||||
database = DatabaseProxy() # use a proxy object, as we will init real db client inside meshchat.py
|
database = DatabaseProxy() # use a proxy object, as we will init real db client inside meshchat.py
|
||||||
migrator = SqliteMigrator(database)
|
migrator = SqliteMigrator(database)
|
||||||
|
|
||||||
@@ -32,6 +32,14 @@ def migrate(current_version):
|
|||||||
migrator.add_column("lxmf_messages", 'method', LxmfMessage.method),
|
migrator.add_column("lxmf_messages", 'method', LxmfMessage.method),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# migrate to version 5
|
||||||
|
if current_version < 5:
|
||||||
|
migrate_database(
|
||||||
|
migrator.add_column("announces", 'rssi', Announce.rssi),
|
||||||
|
migrator.add_column("announces", 'snr', Announce.snr),
|
||||||
|
migrator.add_column("announces", 'quality', Announce.quality),
|
||||||
|
)
|
||||||
|
|
||||||
return latest_version
|
return latest_version
|
||||||
|
|
||||||
|
|
||||||
@@ -61,6 +69,9 @@ class Announce(BaseModel):
|
|||||||
identity_hash = CharField(index=True) # identity hash that announced the destination
|
identity_hash = CharField(index=True) # identity hash that announced the destination
|
||||||
identity_public_key = CharField() # base64 encoded public key, incase we want to recreate the identity manually
|
identity_public_key = CharField() # base64 encoded public key, incase we want to recreate the identity manually
|
||||||
app_data = TextField(null=True) # base64 encoded app data bytes
|
app_data = TextField(null=True) # base64 encoded app data bytes
|
||||||
|
rssi = IntegerField(null=True)
|
||||||
|
snr = FloatField(null=True)
|
||||||
|
quality = FloatField(null=True)
|
||||||
|
|
||||||
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc))
|
created_at = DateTimeField(default=lambda: datetime.now(timezone.utc))
|
||||||
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc))
|
updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc))
|
||||||
|
|||||||
@@ -108,6 +108,26 @@ app.whenReady().then(async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// open external links in default web browser instead of electron
|
||||||
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
|
|
||||||
|
// we want to open call.html in a new electron window
|
||||||
|
// but all other target="_blank" links should open in the system web browser
|
||||||
|
// we don't want /rnode-flasher/index.html to open in electron, otherwise user can't select usb devices...
|
||||||
|
if(url.startsWith("http://localhost") && url.includes("/call.html")){
|
||||||
|
return {
|
||||||
|
action: "allow",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback to opening any other url in external browser
|
||||||
|
shell.openExternal(url);
|
||||||
|
return {
|
||||||
|
action: "deny",
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
// navigate to loading page
|
// navigate to loading page
|
||||||
await mainWindow.loadFile(path.join(__dirname, 'loading.html'));
|
await mainWindow.loadFile(path.join(__dirname, 'loading.html'));
|
||||||
|
|
||||||
|
|||||||
132
meshchat.py
132
meshchat.py
@@ -1047,6 +1047,83 @@ class ReticulumMeshChat:
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# drop path to destination
|
||||||
|
@routes.post("/api/v1/destination/{destination_hash}/drop-path")
|
||||||
|
async def index(request):
|
||||||
|
|
||||||
|
# get path params
|
||||||
|
destination_hash = request.match_info.get("destination_hash", "")
|
||||||
|
|
||||||
|
# convert destination hash to bytes
|
||||||
|
destination_hash = bytes.fromhex(destination_hash)
|
||||||
|
|
||||||
|
# drop path
|
||||||
|
self.reticulum.drop_path(destination_hash)
|
||||||
|
|
||||||
|
return web.json_response({
|
||||||
|
"message": "Path has been dropped",
|
||||||
|
})
|
||||||
|
|
||||||
|
# get signal metrics for a destination by checking the latest announce or lxmf message received from them
|
||||||
|
@routes.get("/api/v1/destination/{destination_hash}/signal-metrics")
|
||||||
|
async def index(request):
|
||||||
|
|
||||||
|
# get path params
|
||||||
|
destination_hash = request.match_info.get("destination_hash", "")
|
||||||
|
|
||||||
|
# signal metrics to return
|
||||||
|
snr = None
|
||||||
|
rssi = None
|
||||||
|
quality = None
|
||||||
|
updated_at = None
|
||||||
|
|
||||||
|
# get latest announce from database for the provided destination hash
|
||||||
|
latest_announce = (database.Announce.select()
|
||||||
|
.where(database.Announce.destination_hash == destination_hash)
|
||||||
|
.get_or_none())
|
||||||
|
|
||||||
|
# get latest lxmf message from database sent to us from the provided destination hash
|
||||||
|
latest_lxmf_message = (database.LxmfMessage.select()
|
||||||
|
.where(database.LxmfMessage.destination_hash == self.local_lxmf_destination.hexhash)
|
||||||
|
.where(database.LxmfMessage.source_hash == destination_hash)
|
||||||
|
.order_by(database.LxmfMessage.id.desc())
|
||||||
|
.get_or_none())
|
||||||
|
|
||||||
|
# determine when latest announce was received
|
||||||
|
latest_announce_at = None
|
||||||
|
if latest_announce is not None:
|
||||||
|
latest_announce_at = datetime.fromisoformat(latest_announce.updated_at)
|
||||||
|
|
||||||
|
# determine when latest lxmf message was received
|
||||||
|
latest_lxmf_message_at = None
|
||||||
|
if latest_lxmf_message is not None:
|
||||||
|
latest_lxmf_message_at = datetime.fromisoformat(latest_lxmf_message.created_at)
|
||||||
|
|
||||||
|
# get signal metrics from latest announce
|
||||||
|
if latest_announce is not None:
|
||||||
|
snr = latest_announce.snr
|
||||||
|
rssi = latest_announce.rssi
|
||||||
|
quality = latest_announce.quality
|
||||||
|
# using updated_at from announce because this is when the latest announce was received
|
||||||
|
updated_at = latest_announce.updated_at
|
||||||
|
|
||||||
|
# get signal metrics from latest lxmf message if it's more recent than the announce
|
||||||
|
if latest_lxmf_message is not None and latest_lxmf_message_at > latest_announce_at:
|
||||||
|
snr = latest_lxmf_message.snr
|
||||||
|
rssi = latest_lxmf_message.rssi
|
||||||
|
quality = latest_lxmf_message.quality
|
||||||
|
# using created_at from lxmf message because this is when the message was received
|
||||||
|
updated_at = latest_lxmf_message.created_at
|
||||||
|
|
||||||
|
return web.json_response({
|
||||||
|
"signal_metrics": {
|
||||||
|
"snr": snr,
|
||||||
|
"rssi": rssi,
|
||||||
|
"quality": quality,
|
||||||
|
"updated_at": updated_at,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
# pings an lxmf.delivery destination by sending empty data and waiting for the recipient to send a proof back
|
# pings an lxmf.delivery destination by sending empty data and waiting for the recipient to send a proof back
|
||||||
# the lxmf router proves all received packets, then drops them if they can't be decoded as lxmf messages
|
# the lxmf router proves all received packets, then drops them if they can't be decoded as lxmf messages
|
||||||
# this allows us to ping/probe any active lxmf.delivery destination and get rtt/snr/rssi data on demand
|
# this allows us to ping/probe any active lxmf.delivery destination and get rtt/snr/rssi data on demand
|
||||||
@@ -1103,8 +1180,9 @@ class ReticulumMeshChat:
|
|||||||
"message": f"Ping failed. Timed out after {timeout_seconds} seconds.",
|
"message": f"Ping failed. Timed out after {timeout_seconds} seconds.",
|
||||||
}, status=503)
|
}, status=503)
|
||||||
|
|
||||||
# get number of hops to destination
|
# get number of hops to destination and back from destination
|
||||||
hops = RNS.Transport.hops_to(destination_hash)
|
hops_there = RNS.Transport.hops_to(destination_hash)
|
||||||
|
hops_back = receipt.proof_packet.hops
|
||||||
|
|
||||||
# get rssi
|
# get rssi
|
||||||
rssi = receipt.proof_packet.rssi
|
rssi = receipt.proof_packet.rssi
|
||||||
@@ -1127,13 +1205,15 @@ class ReticulumMeshChat:
|
|||||||
rtt_duration_string = f"{rtt_milliseconds} ms"
|
rtt_duration_string = f"{rtt_milliseconds} ms"
|
||||||
|
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
"message": f"Valid reply from {receipt.destination.hash.hex()}: hops={hops} time={rtt_duration_string}",
|
"message": f"Valid reply from {receipt.destination.hash.hex()}\nDuration: {rtt_duration_string}\nHops There: {hops_there}\nHops Back: {hops_back}",
|
||||||
"ping_result": {
|
"ping_result": {
|
||||||
"rtt": rtt,
|
"rtt": rtt,
|
||||||
"hops": hops,
|
"hops_there": hops_there,
|
||||||
|
"hops_back": hops_back,
|
||||||
"rssi": rssi,
|
"rssi": rssi,
|
||||||
"snr": snr,
|
"snr": snr,
|
||||||
"quality": quality,
|
"quality": quality,
|
||||||
|
"receiving_interface": str(receipt.proof_packet.receiving_interface),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2009,6 +2089,19 @@ class ReticulumMeshChat:
|
|||||||
elif announce.aspect == "nomadnetwork.node":
|
elif announce.aspect == "nomadnetwork.node":
|
||||||
display_name = self.parse_nomadnetwork_node_display_name(announce.app_data)
|
display_name = self.parse_nomadnetwork_node_display_name(announce.app_data)
|
||||||
|
|
||||||
|
# find lxmf user icon from database
|
||||||
|
lxmf_user_icon = None
|
||||||
|
db_lxmf_user_icon = database.LxmfUserIcon.get_or_none(database.LxmfUserIcon.destination_hash == announce.destination_hash)
|
||||||
|
if db_lxmf_user_icon is not None:
|
||||||
|
lxmf_user_icon = {
|
||||||
|
"icon_name": db_lxmf_user_icon.icon_name,
|
||||||
|
"foreground_colour": db_lxmf_user_icon.foreground_colour,
|
||||||
|
"background_colour": db_lxmf_user_icon.background_colour,
|
||||||
|
}
|
||||||
|
|
||||||
|
# get current hops away
|
||||||
|
hops = RNS.Transport.hops_to(bytes.fromhex(announce.destination_hash))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": announce.id,
|
"id": announce.id,
|
||||||
"destination_hash": announce.destination_hash,
|
"destination_hash": announce.destination_hash,
|
||||||
@@ -2016,8 +2109,13 @@ class ReticulumMeshChat:
|
|||||||
"identity_hash": announce.identity_hash,
|
"identity_hash": announce.identity_hash,
|
||||||
"identity_public_key": announce.identity_public_key,
|
"identity_public_key": announce.identity_public_key,
|
||||||
"app_data": announce.app_data,
|
"app_data": announce.app_data,
|
||||||
|
"hops": hops,
|
||||||
|
"rssi": announce.rssi,
|
||||||
|
"snr": announce.snr,
|
||||||
|
"quality": announce.quality,
|
||||||
"display_name": display_name,
|
"display_name": display_name,
|
||||||
"custom_display_name": self.get_custom_destination_display_name(announce.destination_hash),
|
"custom_display_name": self.get_custom_destination_display_name(announce.destination_hash),
|
||||||
|
"lxmf_user_icon": lxmf_user_icon,
|
||||||
"created_at": announce.created_at,
|
"created_at": announce.created_at,
|
||||||
"updated_at": announce.updated_at,
|
"updated_at": announce.updated_at,
|
||||||
}
|
}
|
||||||
@@ -2175,7 +2273,12 @@ class ReticulumMeshChat:
|
|||||||
query.execute()
|
query.execute()
|
||||||
|
|
||||||
# upserts the provided announce to the database
|
# upserts the provided announce to the database
|
||||||
def db_upsert_announce(self, identity: RNS.Identity, destination_hash: bytes, aspect: str, app_data: bytes):
|
def db_upsert_announce(self, identity: RNS.Identity, destination_hash: bytes, aspect: str, app_data: bytes, announce_packet_hash: bytes):
|
||||||
|
|
||||||
|
# get rssi, snr and signal quality if available
|
||||||
|
rssi = self.reticulum.get_packet_rssi(announce_packet_hash)
|
||||||
|
snr = self.reticulum.get_packet_snr(announce_packet_hash)
|
||||||
|
quality = self.reticulum.get_packet_q(announce_packet_hash)
|
||||||
|
|
||||||
# prepare data to insert or update
|
# prepare data to insert or update
|
||||||
data = {
|
data = {
|
||||||
@@ -2183,6 +2286,9 @@ class ReticulumMeshChat:
|
|||||||
"aspect": aspect,
|
"aspect": aspect,
|
||||||
"identity_hash": identity.hash.hex(),
|
"identity_hash": identity.hash.hex(),
|
||||||
"identity_public_key": base64.b64encode(identity.get_public_key()).decode("utf-8"),
|
"identity_public_key": base64.b64encode(identity.get_public_key()).decode("utf-8"),
|
||||||
|
"rssi": rssi,
|
||||||
|
"snr": snr,
|
||||||
|
"quality": quality,
|
||||||
"updated_at": datetime.now(timezone.utc),
|
"updated_at": datetime.now(timezone.utc),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2366,13 +2472,13 @@ class ReticulumMeshChat:
|
|||||||
|
|
||||||
# handle an announce received from reticulum, for an audio call address
|
# handle an announce received from reticulum, for an audio call address
|
||||||
# NOTE: cant be async, as Reticulum doesn't await it
|
# NOTE: cant be async, as Reticulum doesn't await it
|
||||||
def on_audio_call_announce_received(self, aspect, destination_hash, announced_identity, app_data):
|
def on_audio_call_announce_received(self, aspect, destination_hash, announced_identity, app_data, announce_packet_hash):
|
||||||
|
|
||||||
# log received announce
|
# log received announce
|
||||||
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [call.audio]")
|
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [call.audio]")
|
||||||
|
|
||||||
# upsert announce to database
|
# upsert announce to database
|
||||||
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data)
|
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
||||||
|
|
||||||
# find announce from database
|
# find announce from database
|
||||||
announce = database.Announce.get_or_none(database.Announce.destination_hash == destination_hash.hex())
|
announce = database.Announce.get_or_none(database.Announce.destination_hash == destination_hash.hex())
|
||||||
@@ -2387,13 +2493,13 @@ class ReticulumMeshChat:
|
|||||||
|
|
||||||
# handle an announce received from reticulum, for an lxmf address
|
# handle an announce received from reticulum, for an lxmf address
|
||||||
# NOTE: cant be async, as Reticulum doesn't await it
|
# NOTE: cant be async, as Reticulum doesn't await it
|
||||||
def on_lxmf_announce_received(self, aspect, destination_hash, announced_identity, app_data):
|
def on_lxmf_announce_received(self, aspect, destination_hash, announced_identity, app_data, announce_packet_hash):
|
||||||
|
|
||||||
# log received announce
|
# log received announce
|
||||||
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [lxmf.delivery]")
|
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [lxmf.delivery]")
|
||||||
|
|
||||||
# upsert announce to database
|
# upsert announce to database
|
||||||
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data)
|
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
||||||
|
|
||||||
# find announce from database
|
# find announce from database
|
||||||
announce = database.Announce.get_or_none(database.Announce.destination_hash == destination_hash.hex())
|
announce = database.Announce.get_or_none(database.Announce.destination_hash == destination_hash.hex())
|
||||||
@@ -2412,13 +2518,13 @@ class ReticulumMeshChat:
|
|||||||
|
|
||||||
# handle an announce received from reticulum, for an lxmf propagation node address
|
# handle an announce received from reticulum, for an lxmf propagation node address
|
||||||
# NOTE: cant be async, as Reticulum doesn't await it
|
# NOTE: cant be async, as Reticulum doesn't await it
|
||||||
def on_lxmf_propagation_announce_received(self, aspect, destination_hash, announced_identity, app_data):
|
def on_lxmf_propagation_announce_received(self, aspect, destination_hash, announced_identity, app_data, announce_packet_hash):
|
||||||
|
|
||||||
# log received announce
|
# log received announce
|
||||||
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [lxmf.propagation]")
|
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [lxmf.propagation]")
|
||||||
|
|
||||||
# upsert announce to database
|
# upsert announce to database
|
||||||
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data)
|
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
||||||
|
|
||||||
# find announce from database
|
# find announce from database
|
||||||
announce = database.Announce.get_or_none(database.Announce.destination_hash == destination_hash.hex())
|
announce = database.Announce.get_or_none(database.Announce.destination_hash == destination_hash.hex())
|
||||||
@@ -2496,13 +2602,13 @@ class ReticulumMeshChat:
|
|||||||
|
|
||||||
# handle an announce received from reticulum, for a nomadnet node
|
# handle an announce received from reticulum, for a nomadnet node
|
||||||
# NOTE: cant be async, as Reticulum doesn't await it
|
# NOTE: cant be async, as Reticulum doesn't await it
|
||||||
def on_nomadnet_node_announce_received(self, aspect, destination_hash, announced_identity, app_data):
|
def on_nomadnet_node_announce_received(self, aspect, destination_hash, announced_identity, app_data, announce_packet_hash):
|
||||||
|
|
||||||
# log received announce
|
# log received announce
|
||||||
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [nomadnetwork.node]")
|
print("Received an announce from " + RNS.prettyhexrep(destination_hash) + " for [nomadnetwork.node]")
|
||||||
|
|
||||||
# upsert announce to database
|
# upsert announce to database
|
||||||
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data)
|
self.db_upsert_announce(announced_identity, destination_hash, aspect, app_data, announce_packet_hash)
|
||||||
|
|
||||||
# find announce from database
|
# find announce from database
|
||||||
announce = database.Announce.get_or_none(database.Announce.destination_hash == destination_hash.hex())
|
announce = database.Announce.get_or_none(database.Announce.destination_hash == destination_hash.hex())
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "reticulum-meshchat",
|
"name": "reticulum-meshchat",
|
||||||
"version": "1.14.0",
|
"version": "1.16.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "reticulum-meshchat",
|
"name": "reticulum-meshchat",
|
||||||
"version": "1.14.0",
|
"version": "1.16.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "reticulum-meshchat",
|
"name": "reticulum-meshchat",
|
||||||
"version": "1.14.0",
|
"version": "1.16.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "electron/main.js",
|
"main": "electron/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ class AnnounceHandler:
|
|||||||
self.received_announce_callback = received_announce_callback
|
self.received_announce_callback = received_announce_callback
|
||||||
|
|
||||||
# we will just pass the received announce back to the provided callback
|
# we will just pass the received announce back to the provided callback
|
||||||
def received_announce(self, destination_hash, announced_identity, app_data):
|
def received_announce(self, destination_hash, announced_identity, app_data, announce_packet_hash):
|
||||||
try:
|
try:
|
||||||
# handle received announce
|
# handle received announce
|
||||||
self.received_announce_callback(self.aspect_filter, destination_hash, announced_identity, app_data)
|
self.received_announce_callback(self.aspect_filter, destination_hash, announced_identity, app_data, announce_packet_hash)
|
||||||
except:
|
except:
|
||||||
# ignore failure to handle received announce
|
# ignore failure to handle received announce
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -2,12 +2,10 @@
|
|||||||
<div :class="{'dark': config?.theme === 'dark'}" class="h-screen w-full flex flex-col">
|
<div :class="{'dark': config?.theme === 'dark'}" class="h-screen w-full flex flex-col">
|
||||||
|
|
||||||
<!-- header -->
|
<!-- header -->
|
||||||
<div class="flex bg-white dark:bg-zinc-950 p-2 border-gray-300 dark:border-zinc-900 border-b">
|
<div class="flex bg-white dark:bg-zinc-950 p-2 border-gray-300 dark:border-zinc-900 border-b min-h-16">
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="hidden sm:flex my-auto border border-gray-300 rounded-md w-10 h-10 mr-3 shadow bg-gray-50">
|
<div class="hidden sm:flex my-auto w-12 h-12 mr-2">
|
||||||
<div class="flex mx-auto my-auto">
|
<img class="w-12 h-12" src="/assets/images/logo-chat-bubble.png" />
|
||||||
<img class="w-9 h-9" src="/assets/images/logo.png" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="my-auto">
|
<div class="my-auto">
|
||||||
<div @click="onAppNameClick" class="font-bold cursor-pointer text-gray-900 dark:text-zinc-100">Reticulum MeshChat</div>
|
<div @click="onAppNameClick" class="font-bold cursor-pointer text-gray-900 dark:text-zinc-100">Reticulum MeshChat</div>
|
||||||
@@ -103,6 +101,18 @@
|
|||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<!-- tools -->
|
||||||
|
<li>
|
||||||
|
<SidebarLink :to="{ name: 'tools' }">
|
||||||
|
<template v-slot:icon>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17 17.25 21A2.652 2.652 0 0 0 21 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 1 1-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 0 0 4.486-6.336l-3.276 3.277a3.004 3.004 0 0 1-2.25-2.25l3.276-3.276a4.5 4.5 0 0 0-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437 1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008Z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
<template v-slot:text>Tools</template>
|
||||||
|
</SidebarLink>
|
||||||
|
</li>
|
||||||
|
|
||||||
<!-- settings -->
|
<!-- settings -->
|
||||||
<li>
|
<li>
|
||||||
<SidebarLink :to="{ name: 'settings' }">
|
<SidebarLink :to="{ name: 'settings' }">
|
||||||
|
|||||||
92
src/frontend/components/DropDownMenu.vue
Normal file
92
src/frontend/components/DropDownMenu.vue
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<div v-click-outside="{ handler: onClickOutsideMenu, capture: true }" class="cursor-default relative inline-block text-left">
|
||||||
|
|
||||||
|
<!-- menu button -->
|
||||||
|
<div ref="dropdown-button" @click.stop="toggleMenu">
|
||||||
|
<slot name="button"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- drop down menu -->
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition ease-out duration-100"
|
||||||
|
enter-from-class="transform opacity-0 scale-95"
|
||||||
|
enter-to-class="transform opacity-100 scale-100"
|
||||||
|
leave-active-class="transition ease-in duration-75"
|
||||||
|
leave-from-class="transform opacity-100 scale-100"
|
||||||
|
leave-to-class="transform opacity-0 scale-95">
|
||||||
|
<div v-if="isShowingMenu" @click.stop="hideMenu" class="overflow-hidden absolute right-0 z-10 mr-4 w-56 rounded-md bg-white shadow-md border border-gray-200 focus:outline-none" :class="[ dropdownClass ]">
|
||||||
|
<slot name="items"/>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'DropDownMenu',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isShowingMenu: false,
|
||||||
|
dropdownClass: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleMenu() {
|
||||||
|
if(this.isShowingMenu){
|
||||||
|
this.hideMenu();
|
||||||
|
} else {
|
||||||
|
this.showMenu();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showMenu() {
|
||||||
|
this.isShowingMenu = true;
|
||||||
|
this.adjustDropdownPosition();
|
||||||
|
},
|
||||||
|
hideMenu() {
|
||||||
|
this.isShowingMenu = false;
|
||||||
|
},
|
||||||
|
onClickOutsideMenu(event) {
|
||||||
|
if(this.isShowingMenu){
|
||||||
|
event.preventDefault();
|
||||||
|
this.hideMenu();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
adjustDropdownPosition() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
|
||||||
|
// find button and dropdown
|
||||||
|
const button = this.$refs["dropdown-button"];
|
||||||
|
const dropdown = button.nextElementSibling;
|
||||||
|
|
||||||
|
// do nothing if not found
|
||||||
|
if(!button || !dropdown){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get bounding box of button and dropdown
|
||||||
|
const buttonRect = button.getBoundingClientRect();
|
||||||
|
const dropdownRect = dropdown.getBoundingClientRect();
|
||||||
|
|
||||||
|
// calculate how much space is under and above the button
|
||||||
|
const spaceBelowButton = window.innerHeight - buttonRect.bottom;
|
||||||
|
const spaceAboveButton = buttonRect.top;
|
||||||
|
|
||||||
|
// calculate if there is enough space available to show dropdown
|
||||||
|
const hasEnoughSpaceAboveButton = spaceAboveButton > dropdownRect.height;
|
||||||
|
const hasEnoughSpaceBelowButton = spaceBelowButton > dropdownRect.height;
|
||||||
|
|
||||||
|
// show dropdown above button
|
||||||
|
if(hasEnoughSpaceAboveButton && !hasEnoughSpaceBelowButton){
|
||||||
|
this.dropdownClass = "bottom-0 mb-12";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise fallback to showing dropdown below button
|
||||||
|
this.dropdownClass = "top-0 mt-12";
|
||||||
|
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
11
src/frontend/components/DropDownMenuItem.vue
Normal file
11
src/frontend/components/DropDownMenuItem.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<div class="cursor-pointer flex p-3 space-x-2 text-sm text-gray-500 hover:bg-gray-100">
|
||||||
|
<slot/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'DropDownMenuItem',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
11
src/frontend/components/IconButton.vue
Normal file
11
src/frontend/components/IconButton.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<button type="button" class="p-2 rounded-full text-gray-700 bg-gray-100 hover:bg-gray-200">
|
||||||
|
<slot/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'IconButton',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
142
src/frontend/components/messages/ConversationDropDownMenu.vue
Normal file
142
src/frontend/components/messages/ConversationDropDownMenu.vue
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<template>
|
||||||
|
<DropDownMenu>
|
||||||
|
<template v-slot:button>
|
||||||
|
<IconButton>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 12.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 18.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z" />
|
||||||
|
</svg>
|
||||||
|
</IconButton>
|
||||||
|
</template>
|
||||||
|
<template v-slot:items>
|
||||||
|
|
||||||
|
<!-- call button -->
|
||||||
|
<a :href="`call.html?destination_hash=${peer.destination_hash}`" target="_blank">
|
||||||
|
<DropDownMenuItem>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-5">
|
||||||
|
<path fill-rule="evenodd" d="M1.5 4.5a3 3 0 0 1 3-3h1.372c.86 0 1.61.586 1.819 1.42l1.105 4.423a1.875 1.875 0 0 1-.694 1.955l-1.293.97c-.135.101-.164.249-.126.352a11.285 11.285 0 0 0 6.697 6.697c.103.038.25.009.352-.126l.97-1.293a1.875 1.875 0 0 1 1.955-.694l4.423 1.105c.834.209 1.42.959 1.42 1.82V19.5a3 3 0 0 1-3 3h-2.25C8.552 22.5 1.5 15.448 1.5 6.75V4.5Z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<span>Start a Call</span>
|
||||||
|
</DropDownMenuItem>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- ping button -->
|
||||||
|
<DropDownMenuItem @click="onPingDestination">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-5">
|
||||||
|
<path fill-rule="evenodd" d="M14.615 1.595a.75.75 0 0 1 .359.852L12.982 9.75h7.268a.75.75 0 0 1 .548 1.262l-10.5 11.25a.75.75 0 0 1-1.272-.71l1.992-7.302H3.75a.75.75 0 0 1-.548-1.262l10.5-11.25a.75.75 0 0 1 .913-.143Z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<span>Ping Destination</span>
|
||||||
|
</DropDownMenuItem>
|
||||||
|
|
||||||
|
<!-- set custom display name button -->
|
||||||
|
<DropDownMenuItem @click="onSetCustomDisplayName">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-5">
|
||||||
|
<path fill-rule="evenodd" d="M5.25 2.25a3 3 0 0 0-3 3v4.318a3 3 0 0 0 .879 2.121l9.58 9.581c.92.92 2.39 1.186 3.548.428a18.849 18.849 0 0 0 5.441-5.44c.758-1.16.492-2.629-.428-3.548l-9.58-9.581a3 3 0 0 0-2.122-.879H5.25ZM6.375 7.5a1.125 1.125 0 1 0 0-2.25 1.125 1.125 0 0 0 0 2.25Z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<span>Set Custom Display Name</span>
|
||||||
|
</DropDownMenuItem>
|
||||||
|
|
||||||
|
<!-- delete message history button -->
|
||||||
|
<div class="border-t">
|
||||||
|
<DropDownMenuItem @click="onDeleteMessageHistory">
|
||||||
|
<svg class="size-5 text-red-500" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M8.75 1A2.75 2.75 0 0 0 6 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 1 0 .23 1.482l.149-.022.841 10.518A2.75 2.75 0 0 0 7.596 19h4.807a2.75 2.75 0 0 0 2.742-2.53l.841-10.52.149.023a.75.75 0 0 0 .23-1.482A41.03 41.03 0 0 0 14 4.193V3.75A2.75 2.75 0 0 0 11.25 1h-2.5ZM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4ZM8.58 7.72a.75.75 0 0 0-1.5.06l.3 7.5a.75.75 0 1 0 1.5-.06l-.3-7.5Zm4.34.06a.75.75 0 1 0-1.5-.06l-.3 7.5a.75.75 0 1 0 1.5.06l.3-7.5Z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<span class="text-red-500">Delete Message History</span>
|
||||||
|
</DropDownMenuItem>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</DropDownMenu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DropDownMenu from "../DropDownMenu.vue";
|
||||||
|
import DropDownMenuItem from "../DropDownMenuItem.vue";
|
||||||
|
import IconButton from "../IconButton.vue";
|
||||||
|
import DialogUtils from "../../js/DialogUtils";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ConversationDropDownMenu',
|
||||||
|
components: {
|
||||||
|
IconButton,
|
||||||
|
DropDownMenuItem,
|
||||||
|
DropDownMenu,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
peer: Object,
|
||||||
|
},
|
||||||
|
emits: [
|
||||||
|
"conversation-deleted",
|
||||||
|
"set-custom-display-name",
|
||||||
|
],
|
||||||
|
methods: {
|
||||||
|
async onDeleteMessageHistory() {
|
||||||
|
|
||||||
|
// ask user to confirm deleting conversation history
|
||||||
|
if(!confirm("Are you sure you want to delete all messages in this conversation? This can not be undone!")){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete all lxmf messages from "us to destination" and from "destination to us"
|
||||||
|
try {
|
||||||
|
await window.axios.delete(`/api/v1/lxmf-messages/conversation/${this.peer.destination_hash}`);
|
||||||
|
} catch(e) {
|
||||||
|
DialogUtils.alert("failed to delete conversation");
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fire callback
|
||||||
|
this.$emit("conversation-deleted");
|
||||||
|
|
||||||
|
},
|
||||||
|
async onSetCustomDisplayName() {
|
||||||
|
this.$emit("set-custom-display-name");
|
||||||
|
},
|
||||||
|
async onPingDestination() {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// ping destination
|
||||||
|
const response = await window.axios.get(`/api/v1/ping/${this.peer.destination_hash}/lxmf.delivery`, {
|
||||||
|
params: {
|
||||||
|
timeout: 30,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pingResult = response.data.ping_result;
|
||||||
|
const rttMilliseconds = (pingResult.rtt * 1000).toFixed(3);
|
||||||
|
const rttDurationString = `${rttMilliseconds} ms`;
|
||||||
|
|
||||||
|
const info = [
|
||||||
|
`Valid reply from ${this.peer.destination_hash}`,
|
||||||
|
`Duration: ${rttDurationString}`,
|
||||||
|
`Hops There: ${pingResult.hops_there}`,
|
||||||
|
`Hops Back: ${pingResult.hops_back}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
// add signal quality if available
|
||||||
|
if(pingResult.quality != null){
|
||||||
|
info.push(`Signal Quality: ${pingResult.quality}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add rssi if available
|
||||||
|
if(pingResult.rssi != null){
|
||||||
|
info.push(`RSSI: ${pingResult.rssi}dBm`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add snr if available
|
||||||
|
if(pingResult.snr != null){
|
||||||
|
info.push(`SNR: ${pingResult.snr}dB`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// show result
|
||||||
|
DialogUtils.alert(info.join("\n"));
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
console.log(e);
|
||||||
|
const message = e.response?.data?.message ?? "Ping failed. Try again later";
|
||||||
|
DialogUtils.alert(message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -6,6 +6,16 @@
|
|||||||
<!-- header -->
|
<!-- header -->
|
||||||
<div class="flex p-2 border-b border-gray-300 dark:border-zinc-800">
|
<div class="flex p-2 border-b border-gray-300 dark:border-zinc-800">
|
||||||
|
|
||||||
|
<!-- peer icon -->
|
||||||
|
<div class="my-auto mr-2">
|
||||||
|
<div v-if="selectedPeer.lxmf_user_icon" class="p-2 rounded" :style="{ 'color': selectedPeer.lxmf_user_icon.foreground_colour, 'background-color': selectedPeer.lxmf_user_icon.background_colour }">
|
||||||
|
<MaterialDesignIcon :icon-name="selectedPeer.lxmf_user_icon.icon_name" class="w-6 h-6"/>
|
||||||
|
</div>
|
||||||
|
<div v-else class="bg-gray-200 dark:bg-zinc-700 text-gray-500 dark:text-gray-400 p-2 rounded">
|
||||||
|
<MaterialDesignIcon icon-name="account-outline" class="w-6 h-6"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- peer info -->
|
<!-- peer info -->
|
||||||
<div>
|
<div>
|
||||||
<div @click="updateCustomDisplayName" class="flex cursor-pointer">
|
<div @click="updateCustomDisplayName" class="flex cursor-pointer">
|
||||||
@@ -18,36 +28,46 @@
|
|||||||
<div class="my-auto font-semibold dark:text-white" :title="selectedPeer.display_name">{{ selectedPeer.custom_display_name ?? selectedPeer.display_name }}</div>
|
<div class="my-auto font-semibold dark:text-white" :title="selectedPeer.display_name">{{ selectedPeer.custom_display_name ?? selectedPeer.display_name }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm dark:text-zinc-300">
|
<div class="text-sm dark:text-zinc-300">
|
||||||
<{{ selectedPeer.destination_hash }}>
|
|
||||||
<span v-if="selectedPeerPath" @click="onDestinationPathClick(selectedPeerPath)" class="cursor-pointer">{{ selectedPeerPath.hops }} {{ selectedPeerPath.hops === 1 ? 'hop' : 'hops' }} away</span>
|
<!-- destination hash -->
|
||||||
<span v-if="selectedPeerLxmfStampInfo && selectedPeerLxmfStampInfo.stamp_cost"> • <span @click="onStampInfoClick(selectedPeerLxmfStampInfo)" class="cursor-pointer">Stamp Cost {{ selectedPeerLxmfStampInfo.stamp_cost }}</span></span>
|
<div class="inline-block mr-1">
|
||||||
|
<div><{{ selectedPeer.destination_hash }}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inline-block">
|
||||||
|
<div class="flex space-x-1">
|
||||||
|
|
||||||
|
<!-- hops away -->
|
||||||
|
<span v-if="selectedPeerPath" @click="onDestinationPathClick(selectedPeerPath)" class="flex my-auto cursor-pointer">
|
||||||
|
<span v-if="selectedPeerPath.hops === 0 || selectedPeerPath.hops === 1">Direct</span>
|
||||||
|
<span v-else>{{ selectedPeerPath.hops }} hops away</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- snr -->
|
||||||
|
<span v-if="selectedPeerSignalMetrics?.snr != null" class="flex my-auto space-x-1">
|
||||||
|
<span v-if="selectedPeerPath">•</span>
|
||||||
|
<span @click="onSignalMetricsClick(selectedPeerSignalMetrics)" class="cursor-pointer">SNR {{ selectedPeerSignalMetrics.snr }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- stamp cost -->
|
||||||
|
<span v-if="selectedPeerLxmfStampInfo?.stamp_cost" class="flex my-auto space-x-1">
|
||||||
|
<span v-if="selectedPeerPath || selectedPeerSignalMetrics?.snr != null">•</span>
|
||||||
|
<span @click="onStampInfoClick(selectedPeerLxmfStampInfo)" class="cursor-pointer">Stamp Cost {{ selectedPeerLxmfStampInfo.stamp_cost }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- call button -->
|
<!-- dropdown menu -->
|
||||||
<div class="ml-auto my-auto mr-2">
|
<div class="ml-auto my-auto mx-2">
|
||||||
<a :href="`call.html?destination_hash=${selectedPeer.destination_hash}`" target="_blank" class="cursor-pointer">
|
<ConversationDropDownMenu
|
||||||
<div class="flex text-gray-700 bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
|
v-if="selectedPeer"
|
||||||
<div>
|
:peer="selectedPeer"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
@conversation-deleted="onConversationDeleted"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 0 0 2.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 0 1-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 0 0-1.091-.852H4.5A2.25 2.25 0 0 0 2.25 4.5v2.25Z" />
|
@set-custom-display-name="updateCustomDisplayName"/>
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- delete button -->
|
|
||||||
<div class="my-auto mr-2">
|
|
||||||
<div @click="deleteConversation" class="cursor-pointer">
|
|
||||||
<div class="flex text-gray-700 bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
|
|
||||||
<div>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- close button -->
|
<!-- close button -->
|
||||||
@@ -376,10 +396,14 @@ import WebSocketConnection from "../../js/WebSocketConnection";
|
|||||||
import AddAudioButton from "./AddAudioButton.vue";
|
import AddAudioButton from "./AddAudioButton.vue";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import SendMessageButton from "./SendMessageButton.vue";
|
import SendMessageButton from "./SendMessageButton.vue";
|
||||||
|
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
|
||||||
|
import ConversationDropDownMenu from "./ConversationDropDownMenu.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ConversationViewer',
|
name: 'ConversationViewer',
|
||||||
components: {
|
components: {
|
||||||
|
ConversationDropDownMenu,
|
||||||
|
MaterialDesignIcon,
|
||||||
SendMessageButton,
|
SendMessageButton,
|
||||||
AddAudioButton,
|
AddAudioButton,
|
||||||
},
|
},
|
||||||
@@ -393,6 +417,7 @@ export default {
|
|||||||
|
|
||||||
selectedPeerPath: null,
|
selectedPeerPath: null,
|
||||||
selectedPeerLxmfStampInfo: null,
|
selectedPeerLxmfStampInfo: null,
|
||||||
|
selectedPeerSignalMetrics: null,
|
||||||
|
|
||||||
lxmfMessagesRequestSequence: 0,
|
lxmfMessagesRequestSequence: 0,
|
||||||
chatItems: [],
|
chatItems: [],
|
||||||
@@ -465,12 +490,16 @@ export default {
|
|||||||
// reset
|
// reset
|
||||||
this.chatItems = [];
|
this.chatItems = [];
|
||||||
this.hasMorePrevious = true;
|
this.hasMorePrevious = true;
|
||||||
|
this.selectedPeerPath = null;
|
||||||
|
this.selectedPeerLxmfStampInfo = null;
|
||||||
|
this.selectedPeerSignalMetrics = null;
|
||||||
if(!this.selectedPeer){
|
if(!this.selectedPeer){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.getPeerPath();
|
this.getPeerPath();
|
||||||
this.getPeerLxmfStampInfo();
|
this.getPeerLxmfStampInfo();
|
||||||
|
this.getPeerSignalMetrics();
|
||||||
|
|
||||||
// load 1 page of previous messages
|
// load 1 page of previous messages
|
||||||
await this.loadPrevious();
|
await this.loadPrevious();
|
||||||
@@ -540,15 +569,18 @@ export default {
|
|||||||
const json = JSON.parse(message.data);
|
const json = JSON.parse(message.data);
|
||||||
switch(json.type){
|
switch(json.type){
|
||||||
case 'announce': {
|
case 'announce': {
|
||||||
// update stamp info if an announce is received from the selected peer
|
// update stamp info and signal metrics if an announce is received from the selected peer
|
||||||
if(json.announce.destination_hash === this.selectedPeer?.destination_hash){
|
if(json.announce.destination_hash === this.selectedPeer?.destination_hash){
|
||||||
|
await this.getPeerPath();
|
||||||
await this.getPeerLxmfStampInfo();
|
await this.getPeerLxmfStampInfo();
|
||||||
|
await this.getPeerSignalMetrics();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'lxmf.delivery': {
|
case 'lxmf.delivery': {
|
||||||
this.onLxmfMessageReceived(json.lxmf_message);
|
this.onLxmfMessageReceived(json.lxmf_message);
|
||||||
await this.getPeerPath();
|
await this.getPeerPath();
|
||||||
|
await this.getPeerSignalMetrics();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'lxmf_message_created': {
|
case 'lxmf_message_created': {
|
||||||
@@ -632,10 +664,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getPeerPath() {
|
async getPeerPath() {
|
||||||
|
|
||||||
// clear previous known path
|
|
||||||
this.selectedPeerPath = null;
|
|
||||||
|
|
||||||
if(this.selectedPeer){
|
if(this.selectedPeer){
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@@ -647,15 +675,14 @@ export default {
|
|||||||
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
|
|
||||||
|
// clear previous known path
|
||||||
|
this.selectedPeerPath = null;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
async getPeerLxmfStampInfo() {
|
async getPeerLxmfStampInfo() {
|
||||||
|
|
||||||
// clear previous stamp info
|
|
||||||
this.selectedPeerLxmfStampInfo = null;
|
|
||||||
|
|
||||||
if(this.selectedPeer){
|
if(this.selectedPeer){
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@@ -666,10 +693,34 @@ export default {
|
|||||||
this.selectedPeerLxmfStampInfo = response.data.lxmf_stamp_info;
|
this.selectedPeerLxmfStampInfo = response.data.lxmf_stamp_info;
|
||||||
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
|
||||||
console.log(e);
|
console.log(e);
|
||||||
|
|
||||||
|
// clear previous stamp info
|
||||||
|
this.selectedPeerLxmfStampInfo = null;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async getPeerSignalMetrics() {
|
||||||
|
if(this.selectedPeer){
|
||||||
|
try {
|
||||||
|
|
||||||
|
// get signal metrics
|
||||||
|
const response = await window.axios.get(`/api/v1/destination/${this.selectedPeer.destination_hash}/signal-metrics`);
|
||||||
|
|
||||||
|
// update ui
|
||||||
|
this.selectedPeerSignalMetrics = response.data.signal_metrics;
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
|
||||||
|
console.log(e);
|
||||||
|
|
||||||
|
// clear previous signal metrics
|
||||||
|
this.selectedPeerSignalMetrics = null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onDestinationPathClick(path) {
|
onDestinationPathClick(path) {
|
||||||
DialogUtils.alert(`${path.hops} ${ path.hops === 1 ? 'hop' : 'hops' } away via ${path.next_hop_interface}`);
|
DialogUtils.alert(`${path.hops} ${ path.hops === 1 ? 'hop' : 'hops' } away via ${path.next_hop_interface}`);
|
||||||
@@ -709,6 +760,13 @@ export default {
|
|||||||
DialogUtils.alert(`This peer has enabled stamp security.\n\nYour device must have a ticket, or solve an automated proof of work task each time you send them a message.\n\nTime per message: ${estimatedTimeForStamp}`);
|
DialogUtils.alert(`This peer has enabled stamp security.\n\nYour device must have a ticket, or solve an automated proof of work task each time you send them a message.\n\nTime per message: ${estimatedTimeForStamp}`);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
onSignalMetricsClick(signalMetrics) {
|
||||||
|
DialogUtils.alert([
|
||||||
|
`Signal Quality: ${ signalMetrics.quality ?? '???' }%`,
|
||||||
|
`RSSI: ${ signalMetrics.rssi ?? '???' }dBm`,
|
||||||
|
`SNR: ${ signalMetrics.snr ?? '???'}dB`,
|
||||||
|
].join("\n"));
|
||||||
|
},
|
||||||
scrollMessagesToBottom: function() {
|
scrollMessagesToBottom: function() {
|
||||||
// next tick waits for the ui to have the new elements added
|
// next tick waits for the ui to have the new elements added
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -771,25 +829,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
async deleteConversation() {
|
async onConversationDeleted() {
|
||||||
|
|
||||||
// do nothing if no peer selected
|
|
||||||
if(!this.selectedPeer){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ask user to confirm deleting conversation history
|
|
||||||
if(!confirm("Are you sure you want to delete all messages from this conversation? This can not be undone!")){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete all lxmf messages from "us to destination" and from "destination to us"
|
|
||||||
try {
|
|
||||||
await window.axios.delete(`/api/v1/lxmf-messages/conversation/${this.selectedPeer.destination_hash}`);
|
|
||||||
} catch(e) {
|
|
||||||
DialogUtils.alert("failed to delete conversation");
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// reload conversation
|
// reload conversation
|
||||||
await this.initialLoad();
|
await this.initialLoad();
|
||||||
|
|||||||
@@ -26,9 +26,7 @@
|
|||||||
<MaterialDesignIcon :icon-name="conversation.lxmf_user_icon.icon_name" class="w-6 h-6"/>
|
<MaterialDesignIcon :icon-name="conversation.lxmf_user_icon.icon_name" class="w-6 h-6"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="bg-gray-200 dark:bg-zinc-700 text-gray-500 dark:text-gray-400 p-2 rounded">
|
<div v-else class="bg-gray-200 dark:bg-zinc-700 text-gray-500 dark:text-gray-400 p-2 rounded">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
<MaterialDesignIcon icon-name="account-outline" class="w-6 h-6"/>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mr-auto">
|
<div class="mr-auto">
|
||||||
@@ -83,15 +81,36 @@
|
|||||||
<div v-if="searchedPeers.length > 0" class="w-full">
|
<div v-if="searchedPeers.length > 0" class="w-full">
|
||||||
<div @click="onPeerClick(peer)" v-for="peer of searchedPeers" class="flex cursor-pointer p-2 border-l-2" :class="[ peer.destination_hash === selectedDestinationHash ? 'bg-gray-100 dark:bg-zinc-700 border-blue-500 dark:border-blue-400' : 'bg-white dark:bg-zinc-950 border-transparent hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-gray-200 dark:hover:border-zinc-600' ]">
|
<div @click="onPeerClick(peer)" v-for="peer of searchedPeers" class="flex cursor-pointer p-2 border-l-2" :class="[ peer.destination_hash === selectedDestinationHash ? 'bg-gray-100 dark:bg-zinc-700 border-blue-500 dark:border-blue-400' : 'bg-white dark:bg-zinc-950 border-transparent hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-gray-200 dark:hover:border-zinc-600' ]">
|
||||||
<div class="my-auto mr-2">
|
<div class="my-auto mr-2">
|
||||||
<div class="bg-gray-200 dark:bg-zinc-700 text-gray-500 dark:text-gray-400 p-2 rounded">
|
<div v-if="peer.lxmf_user_icon" class="p-2 rounded" :style="{ 'color': peer.lxmf_user_icon.foreground_colour, 'background-color': peer.lxmf_user_icon.background_colour }">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
<MaterialDesignIcon :icon-name="peer.lxmf_user_icon.icon_name" class="w-6 h-6"/>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
</div>
|
||||||
</svg>
|
<div v-else class="bg-gray-200 dark:bg-zinc-700 text-gray-500 dark:text-gray-400 p-2 rounded">
|
||||||
|
<MaterialDesignIcon icon-name="account-outline" class="w-6 h-6"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-gray-900 dark:text-gray-100">{{ peer.custom_display_name ?? peer.display_name }}</div>
|
<div class="text-gray-900 dark:text-gray-100">{{ peer.custom_display_name ?? peer.display_name }}</div>
|
||||||
<div class="text-gray-500 dark:text-gray-400 text-sm">{{ formatTimeAgo(peer.updated_at) }}</div>
|
<div class="flex space-x-1 text-gray-500 dark:text-gray-400 text-sm">
|
||||||
|
|
||||||
|
<!-- time ago -->
|
||||||
|
<span class="flex my-auto space-x-1">
|
||||||
|
{{ formatTimeAgo(peer.updated_at) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- hops away -->
|
||||||
|
<span v-if="peer.hops != null && peer.hops !== 128" class="flex my-auto text-sm text-gray-500 space-x-1">
|
||||||
|
<span>•</span>
|
||||||
|
<span v-if="peer.hops === 0 || peer.hops === 1">Direct</span>
|
||||||
|
<span v-else>{{ peer.hops }} hops</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- snr -->
|
||||||
|
<span v-if="peer.snr != null" class="flex my-auto space-x-1">
|
||||||
|
<span>•</span>
|
||||||
|
<span>SNR {{ peer.snr }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,9 +25,7 @@
|
|||||||
>
|
>
|
||||||
<div class="my-auto mr-2">
|
<div class="my-auto mr-2">
|
||||||
<div class="bg-gray-200 dark:bg-zinc-800 text-gray-500 dark:text-gray-400 p-2 rounded">
|
<div class="bg-gray-200 dark:bg-zinc-800 text-gray-500 dark:text-gray-400 p-2 rounded">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
<MaterialDesignIcon icon-name="server-network-outline" class="w-6 h-6"/>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 14.25h13.5m-13.5 0a3 3 0 0 1-3-3m3 3a3 3 0 1 0 0 6h13.5a3 3 0 1 0 0-6m-16.5-3a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3m-19.5 0a4.5 4.5 0 0 1 .9-2.7L5.737 5.1a3.375 3.375 0 0 1 2.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 0 1 .9 2.7m0 0a3 3 0 0 1-3 3m0 3h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Zm-3 6h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -66,9 +64,11 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
import Utils from "../../js/Utils";
|
import Utils from "../../js/Utils";
|
||||||
|
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'NomadNetworkSidebar',
|
name: 'NomadNetworkSidebar',
|
||||||
|
components: {MaterialDesignIcon},
|
||||||
props: {
|
props: {
|
||||||
nodes: Object,
|
nodes: Object,
|
||||||
selectedDestinationHash: String,
|
selectedDestinationHash: String,
|
||||||
|
|||||||
223
src/frontend/components/ping/PingPage.vue
Normal file
223
src/frontend/components/ping/PingPage.vue
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col flex-1 overflow-hidden min-w-full sm:min-w-[500px] dark:bg-zinc-950">
|
||||||
|
<div class="flex flex-col h-full space-y-2 p-2 overflow-y-auto">
|
||||||
|
|
||||||
|
<!-- appearance -->
|
||||||
|
<div class="bg-white dark:bg-zinc-800 rounded shadow">
|
||||||
|
<div class="flex border-b border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-gray-200 p-2 font-semibold">Ping</div>
|
||||||
|
<div class="dark:divide-zinc-700 text-gray-900 dark:text-gray-100 p-2">
|
||||||
|
Only lxmf.delivery destinations can be pinged.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- inputs -->
|
||||||
|
<div class="bg-white dark:bg-zinc-800 rounded shadow">
|
||||||
|
<div class="divide-y divide-gray-300 dark:divide-zinc-700 text-gray-900 dark:text-gray-100">
|
||||||
|
|
||||||
|
<div class="p-2">
|
||||||
|
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">Destination Hash</div>
|
||||||
|
<div class="flex">
|
||||||
|
<input v-model="destinationHash" type="text" placeholder="e.g: 7b746057a7294469799cd8d7d429676a" class="bg-gray-50 dark:bg-zinc-700 border border-gray-300 dark:border-zinc-600 text-gray-900 dark:text-gray-100 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-blue-600 dark:focus:border-blue-600 block w-full p-2.5">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-2">
|
||||||
|
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">Ping Timeout (seconds)</div>
|
||||||
|
<div class="flex">
|
||||||
|
<input v-model="timeout" type="number" placeholder="Timeout" class="bg-gray-50 dark:bg-zinc-700 border border-gray-300 dark:border-zinc-600 text-gray-900 dark:text-gray-100 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-blue-600 dark:focus:border-blue-600 block w-full p-2.5">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-2 space-x-1">
|
||||||
|
<button v-if="!isRunning" @click="start" type="button" class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-700 dark:text-white dark:hover:bg-zinc-600 dark:focus-visible:outline-zinc-500">
|
||||||
|
Start
|
||||||
|
</button>
|
||||||
|
<button v-if="isRunning" @click="stop" type="button" class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-700 dark:text-white dark:hover:bg-zinc-600 dark:focus-visible:outline-zinc-500">
|
||||||
|
Stop
|
||||||
|
</button>
|
||||||
|
<button @click="clear" type="button" class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-700 dark:text-white dark:hover:bg-zinc-600 dark:focus-visible:outline-zinc-500">
|
||||||
|
Clear Results
|
||||||
|
</button>
|
||||||
|
<button @click="dropPath" type="button" class="my-auto inline-flex items-center gap-x-1 rounded-md bg-red-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-red-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500">
|
||||||
|
Drop Path
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- results -->
|
||||||
|
<div class="flex flex-col h-full bg-white dark:bg-zinc-800 rounded shadow overflow-hidden min-h-52">
|
||||||
|
<div class="flex border-b border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-gray-200 p-2 font-semibold">Results</div>
|
||||||
|
<div id="results" class="flex flex-col h-full bg-black text-white dark:bg-zinc-800 dark:text-gray-200 p-2 overflow-y-scroll font-mono">
|
||||||
|
<div v-for="pingResult of pingResults">{{ pingResult }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {CanceledError} from "axios";
|
||||||
|
import DialogUtils from "../../js/DialogUtils";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PingPage',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isRunning: false,
|
||||||
|
destinationHash: null,
|
||||||
|
timeout: 10,
|
||||||
|
seq: 0,
|
||||||
|
pingResults: [],
|
||||||
|
abortController: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.stop();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async start() {
|
||||||
|
|
||||||
|
// do nothing if already running
|
||||||
|
if(this.isRunning){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// simple check to ensure destination hash is valid
|
||||||
|
if(this.destinationHash == null || this.destinationHash.length !== 32){
|
||||||
|
DialogUtils.alert("Invalid Destination Hash!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// simple check to ensure destination hash is valid
|
||||||
|
if(this.timeout == null || this.timeout < 0){
|
||||||
|
DialogUtils.alert("Timeout must be a number!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we are now running ping
|
||||||
|
this.seq = 0;
|
||||||
|
this.isRunning = true;
|
||||||
|
this.abortController = new AbortController();
|
||||||
|
|
||||||
|
// run ping until stopped
|
||||||
|
while(this.isRunning){
|
||||||
|
|
||||||
|
// run ping
|
||||||
|
await this.ping();
|
||||||
|
|
||||||
|
// wait a bit before running next ping
|
||||||
|
await this.sleep(1000);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
async stop() {
|
||||||
|
this.isRunning = false;
|
||||||
|
this.abortController.abort();
|
||||||
|
},
|
||||||
|
async clear() {
|
||||||
|
this.pingResults = [];
|
||||||
|
},
|
||||||
|
async sleep(millis) {
|
||||||
|
return new Promise((resolve, reject) => setTimeout(resolve, millis));
|
||||||
|
},
|
||||||
|
async ping() {
|
||||||
|
try {
|
||||||
|
|
||||||
|
this.seq++;
|
||||||
|
|
||||||
|
// ping destination
|
||||||
|
const response = await window.axios.get(`/api/v1/ping/${this.destinationHash}/lxmf.delivery`, {
|
||||||
|
signal: this.abortController.signal,
|
||||||
|
params: {
|
||||||
|
timeout: this.timeout,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pingResult = response.data.ping_result;
|
||||||
|
const rttMilliseconds = (pingResult.rtt * 1000).toFixed(3);
|
||||||
|
const rttDurationString = `${rttMilliseconds}ms`;
|
||||||
|
|
||||||
|
const info = [
|
||||||
|
`seq=${this.seq}`,
|
||||||
|
`duration=${rttDurationString}`,
|
||||||
|
`hops_there=${pingResult.hops_there}`,
|
||||||
|
`hops_back=${pingResult.hops_back}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
// add rssi if available
|
||||||
|
if(pingResult.rssi != null){
|
||||||
|
info.push(`rssi=${pingResult.rssi}dBm`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add snr if available
|
||||||
|
if(pingResult.snr != null){
|
||||||
|
info.push(`snr=${pingResult.snr}dB`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add signal quality if available
|
||||||
|
if(pingResult.quality != null){
|
||||||
|
info.push(`quality=${pingResult.quality}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add receiving interface
|
||||||
|
info.push(`via=${pingResult.receiving_interface}`);
|
||||||
|
|
||||||
|
// update ui
|
||||||
|
this.addPingResult(info.join(" "));
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
|
||||||
|
// ignore cancelled error
|
||||||
|
if(e instanceof CanceledError){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(e);
|
||||||
|
|
||||||
|
// add ping error to results
|
||||||
|
const message = e.response?.data?.message ?? e;
|
||||||
|
this.addPingResult(`seq=${this.seq} error=${message}`);
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async dropPath() {
|
||||||
|
|
||||||
|
// simple check to ensure destination hash is valid
|
||||||
|
if(this.destinationHash == null || this.destinationHash.length !== 32){
|
||||||
|
DialogUtils.alert("Invalid Destination Hash!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await window.axios.post(`/api/v1/destination/${this.destinationHash}/drop-path`);
|
||||||
|
DialogUtils.alert(response.data.message);
|
||||||
|
} catch(e) {
|
||||||
|
console.log(e);
|
||||||
|
const message = e.response?.data?.message ?? `Failed to drop path: ${e}`;
|
||||||
|
DialogUtils.alert(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
addPingResult(result) {
|
||||||
|
this.pingResults.push(result);
|
||||||
|
this.scrollPingResultsToBottom();
|
||||||
|
},
|
||||||
|
scrollPingResultsToBottom: function() {
|
||||||
|
// next tick waits for the ui to have the new elements added
|
||||||
|
this.$nextTick(() => {
|
||||||
|
// set timeout with zero millis seems to fix issue where it doesn't scroll all the way to the bottom...
|
||||||
|
setTimeout(() => {
|
||||||
|
const container = document.getElementById("results");
|
||||||
|
if(container){
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
60
src/frontend/components/tools/ToolsPage.vue
Normal file
60
src/frontend/components/tools/ToolsPage.vue
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col flex-1 overflow-hidden min-w-full sm:min-w-[500px] dark:bg-zinc-950">
|
||||||
|
<div class="overflow-y-auto space-y-2 p-2">
|
||||||
|
|
||||||
|
<!-- appearance -->
|
||||||
|
<div class="bg-white dark:bg-zinc-800 rounded shadow">
|
||||||
|
<div class="flex border-b border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-gray-200 p-2 font-semibold">Tools</div>
|
||||||
|
<div class="dark:divide-zinc-700 text-gray-900 dark:text-gray-100 p-2">
|
||||||
|
A collection of useful tools bundled with MeshChat
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ping -->
|
||||||
|
<RouterLink :to="{ name: 'ping' }" class="group flex bg-white dark:bg-zinc-800 p-2 rounded shadow hover:bg-gray-50 dark:hover:bg-zinc-700">
|
||||||
|
<div class="mr-2">
|
||||||
|
<div class="flex bg-gray-300 text-gray-500 rounded shadow p-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-10">
|
||||||
|
<path fill-rule="evenodd" d="M14.615 1.595a.75.75 0 0 1 .359.852L12.982 9.75h7.268a.75.75 0 0 1 .548 1.262l-10.5 11.25a.75.75 0 0 1-1.272-.71l1.992-7.302H3.75a.75.75 0 0 1-.548-1.262l10.5-11.25a.75.75 0 0 1 .913-.143Z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="my-auto mr-auto dark:text-gray-200">
|
||||||
|
<div class="font-bold">Ping</div>
|
||||||
|
<div class="text-sm">Allows you to ping a destination hash.</div>
|
||||||
|
</div>
|
||||||
|
<div class="my-auto text-gray-400 group-hover:text-gray-500">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
|
<!-- rnode flasher -->
|
||||||
|
<a target="_blank" href="/rnode-flasher/index.html" class="group flex bg-white dark:bg-zinc-800 p-2 rounded shadow hover:bg-gray-50 dark:hover:bg-zinc-700">
|
||||||
|
<div class="mr-2">
|
||||||
|
<div class="flex bg-gray-300 text-white rounded shadow">
|
||||||
|
<img src="/rnode-flasher/reticulum_logo_512.png" class="size-14"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="my-auto mr-auto dark:text-gray-200">
|
||||||
|
<div class="font-bold">RNode Flasher</div>
|
||||||
|
<div class="text-sm">Flash RNode firmware to supported devices.</div>
|
||||||
|
</div>
|
||||||
|
<div class="my-auto text-gray-400 group-hover:text-gray-500">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ToolsPage',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -54,9 +54,9 @@ class Utils {
|
|||||||
|
|
||||||
if(parsedSeconds.minutes > 0){
|
if(parsedSeconds.minutes > 0){
|
||||||
if(parsedSeconds.minutes === 1){
|
if(parsedSeconds.minutes === 1){
|
||||||
return "a minute ago";
|
return "1 min ago";
|
||||||
} else {
|
} else {
|
||||||
return parsedSeconds.minutes + " minutes ago";
|
return parsedSeconds.minutes + " mins ago";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,11 +60,21 @@ const router = createRouter({
|
|||||||
path: '/propagation-nodes',
|
path: '/propagation-nodes',
|
||||||
component: defineAsyncComponent(() => import("./components/propagation-nodes/PropagationNodesPage.vue")),
|
component: defineAsyncComponent(() => import("./components/propagation-nodes/PropagationNodesPage.vue")),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ping",
|
||||||
|
path: '/ping',
|
||||||
|
component: defineAsyncComponent(() => import("./components/ping/PingPage.vue")),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "settings",
|
name: "settings",
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
component: defineAsyncComponent(() => import("./components/settings/SettingsPage.vue")),
|
component: defineAsyncComponent(() => import("./components/settings/SettingsPage.vue")),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "tools",
|
||||||
|
path: '/tools',
|
||||||
|
component: defineAsyncComponent(() => import("./components/tools/ToolsPage.vue")),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
BIN
src/frontend/public/assets/images/logo-chat-bubble.png
Normal file
BIN
src/frontend/public/assets/images/logo-chat-bubble.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
21
src/frontend/public/rnode-flasher/LICENSE
Normal file
21
src/frontend/public/rnode-flasher/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Liam Cottle
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
86
src/frontend/public/rnode-flasher/README.md
Normal file
86
src/frontend/public/rnode-flasher/README.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# RNode Flasher
|
||||||
|
|
||||||
|
A _work-in-progress_ web based firmware flasher for [Reticulum](https://github.com/markqvist/Reticulum) / [RNode_Firmware](https://github.com/markqvist/RNode_Firmware).
|
||||||
|
|
||||||
|
- It is written in javascript and uses the [Web Serial APIs](https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API).
|
||||||
|
- It supports putting relevant devices into DFU mode.
|
||||||
|
- It supports flashing firmware from a zip file.
|
||||||
|
|
||||||
|
At this time, it does not support flashing bootloaders or softdevices for the nRF boards.
|
||||||
|
|
||||||
|
## How does it work?
|
||||||
|
|
||||||
|
I wanted something simple, for flashing RNode firmware to a nRF52 RAK4631 in a web browser.
|
||||||
|
|
||||||
|
So, I spent a bit of time working through the source code of [adafruit-nrfutil](https://github.com/adafruit/Adafruit_nRF52_nrfutil) and wrote a javascript implementation of [dfu_transport_serial.py](https://github.com/adafruit/Adafruit_nRF52_nrfutil/blob/master/nordicsemi/dfu/dfu_transport_serial.py)
|
||||||
|
|
||||||
|
Generally, you would use the following command to flash a firmware.zip to your device;
|
||||||
|
|
||||||
|
```
|
||||||
|
adafruit-nrfutil dfu serial --package firmware.zip -p /dev/cu.usbmodem14401 -b 115200 -t 1200
|
||||||
|
```
|
||||||
|
|
||||||
|
The [nrf52_dfu_flasher.js](js/nrf52_dfu_flasher.js) in this project implements a javascript, web based version of the above command.
|
||||||
|
|
||||||
|
There was an existing package called [pc-nrf-dfu-js](https://github.com/NordicSemiconductor/pc-nrf-dfu-js), however this repo had been archived and didn't appear to support the latest DFU protocol.
|
||||||
|
|
||||||
|
## How to use it?
|
||||||
|
|
||||||
|
- Open https://liamcottle.github.io/rnode-flasher/ in your web browser.
|
||||||
|
- Select your device.
|
||||||
|
- Put your device into DFU mode (for nRF52 boards)
|
||||||
|
- Select a firmware file and click flash.
|
||||||
|
- Once flashed, your device should reboot into the new firmware.
|
||||||
|
- For new devices that have never been provisioned, you should click "Provision" to configure the EEPROM.
|
||||||
|
- Every time you flash new firmware, you should also click "Set Firmware Hash".
|
||||||
|
|
||||||
|
> Note: At this time, firmware hashes for RNode are not automatically configured.
|
||||||
|
|
||||||
|
## What is needed to set up a new RNode?
|
||||||
|
|
||||||
|
> Note: This is a technical overview of how the RNode device provisioning works.
|
||||||
|
> Most of this is taken care of by the code base, and this section just makes it easier to understand what is going on.
|
||||||
|
|
||||||
|
To set up a new RNode device, you will need to do a few things;
|
||||||
|
|
||||||
|
- Obtain supported hardware, such as a RAK4631
|
||||||
|
- Obtain an RNode firmware file
|
||||||
|
- Put your device into DFU mode
|
||||||
|
- Flash the firmware file
|
||||||
|
- Provision the EEPROM
|
||||||
|
|
||||||
|
Once the firmware is flashed to the device, you will need to provision the EEPROM;
|
||||||
|
|
||||||
|
- Set firmware hash in eeprom
|
||||||
|
- Collect device info
|
||||||
|
- `product`
|
||||||
|
- `model`
|
||||||
|
- `hardware_revision`
|
||||||
|
- `serial_number`
|
||||||
|
- `made` (unix timestamp of device creation)
|
||||||
|
- Write device info to eeprom
|
||||||
|
- Create an MD5 checksum of the device info
|
||||||
|
- Write 16 byte device info checksum to eeprom
|
||||||
|
- Sign device info checksum with signing key to use as signature
|
||||||
|
- Write 128 byte signature to eeprom
|
||||||
|
- Write `ROM.INFO_LOCK_BYTE` to `ROM.ADDR_INFO_LOCK` in eeprom
|
||||||
|
- Read eeprom and validate checksums and signatures to ensure all is correct
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- support configuring eeprom with device signatures and firmware hashes
|
||||||
|
- support flashing existing firmware files from api
|
||||||
|
- calculate on air bitrate based on tnc settings
|
||||||
|
- try using [web-serial-polyfill](https://github.com/google/web-serial-polyfill) to support flashing from Android device?
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- https://github.com/adafruit/Adafruit_nRF52_nrfutil
|
||||||
|
- https://github.com/adafruit/Adafruit_nRF52_nrfutil/blob/master/nordicsemi/dfu/dfu_transport_serial.py
|
||||||
|
- https://github.com/markqvist/RNode_Firmware/blob/master/RNode_Firmware.ino
|
||||||
|
- https://github.com/markqvist/RNode_Firmware/blob/master/Framing.h
|
||||||
|
- https://github.com/markqvist/RNode_Firmware/blob/master/Utilities.h
|
||||||
1783
src/frontend/public/rnode-flasher/index.html
Normal file
1783
src/frontend/public/rnode-flasher/index.html
Normal file
File diff suppressed because it is too large
Load Diff
760
src/frontend/public/rnode-flasher/js/crypto-js@3.9.1-1/core.js
Normal file
760
src/frontend/public/rnode-flasher/js/crypto-js@3.9.1-1/core.js
Normal file
@@ -0,0 +1,760 @@
|
|||||||
|
;(function (root, factory) {
|
||||||
|
if (typeof exports === "object") {
|
||||||
|
// CommonJS
|
||||||
|
module.exports = exports = factory();
|
||||||
|
}
|
||||||
|
else if (typeof define === "function" && define.amd) {
|
||||||
|
// AMD
|
||||||
|
define([], factory);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Global (browser)
|
||||||
|
root.CryptoJS = factory();
|
||||||
|
}
|
||||||
|
}(this, function () {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CryptoJS core components.
|
||||||
|
*/
|
||||||
|
var CryptoJS = CryptoJS || (function (Math, undefined) {
|
||||||
|
/*
|
||||||
|
* Local polyfil of Object.create
|
||||||
|
*/
|
||||||
|
var create = Object.create || (function () {
|
||||||
|
function F() {};
|
||||||
|
|
||||||
|
return function (obj) {
|
||||||
|
var subtype;
|
||||||
|
|
||||||
|
F.prototype = obj;
|
||||||
|
|
||||||
|
subtype = new F();
|
||||||
|
|
||||||
|
F.prototype = null;
|
||||||
|
|
||||||
|
return subtype;
|
||||||
|
};
|
||||||
|
}())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CryptoJS namespace.
|
||||||
|
*/
|
||||||
|
var C = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Library namespace.
|
||||||
|
*/
|
||||||
|
var C_lib = C.lib = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base object for prototypal inheritance.
|
||||||
|
*/
|
||||||
|
var Base = C_lib.Base = (function () {
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Creates a new object that inherits from this object.
|
||||||
|
*
|
||||||
|
* @param {Object} overrides Properties to copy into the new object.
|
||||||
|
*
|
||||||
|
* @return {Object} The new object.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var MyType = CryptoJS.lib.Base.extend({
|
||||||
|
* field: 'value',
|
||||||
|
*
|
||||||
|
* method: function () {
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
extend: function (overrides) {
|
||||||
|
// Spawn
|
||||||
|
var subtype = create(this);
|
||||||
|
|
||||||
|
// Augment
|
||||||
|
if (overrides) {
|
||||||
|
subtype.mixIn(overrides);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create default initializer
|
||||||
|
if (!subtype.hasOwnProperty('init') || this.init === subtype.init) {
|
||||||
|
subtype.init = function () {
|
||||||
|
subtype.$super.init.apply(this, arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializer's prototype is the subtype object
|
||||||
|
subtype.init.prototype = subtype;
|
||||||
|
|
||||||
|
// Reference supertype
|
||||||
|
subtype.$super = this;
|
||||||
|
|
||||||
|
return subtype;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends this object and runs the init method.
|
||||||
|
* Arguments to create() will be passed to init().
|
||||||
|
*
|
||||||
|
* @return {Object} The new object.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var instance = MyType.create();
|
||||||
|
*/
|
||||||
|
create: function () {
|
||||||
|
var instance = this.extend();
|
||||||
|
instance.init.apply(instance, arguments);
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a newly created object.
|
||||||
|
* Override this method to add some logic when your objects are created.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var MyType = CryptoJS.lib.Base.extend({
|
||||||
|
* init: function () {
|
||||||
|
* // ...
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
init: function () {
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies properties into this object.
|
||||||
|
*
|
||||||
|
* @param {Object} properties The properties to mix in.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* MyType.mixIn({
|
||||||
|
* field: 'value'
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
mixIn: function (properties) {
|
||||||
|
for (var propertyName in properties) {
|
||||||
|
if (properties.hasOwnProperty(propertyName)) {
|
||||||
|
this[propertyName] = properties[propertyName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IE won't copy toString using the loop above
|
||||||
|
if (properties.hasOwnProperty('toString')) {
|
||||||
|
this.toString = properties.toString;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a copy of this object.
|
||||||
|
*
|
||||||
|
* @return {Object} The clone.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var clone = instance.clone();
|
||||||
|
*/
|
||||||
|
clone: function () {
|
||||||
|
return this.init.prototype.extend(this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of 32-bit words.
|
||||||
|
*
|
||||||
|
* @property {Array} words The array of 32-bit words.
|
||||||
|
* @property {number} sigBytes The number of significant bytes in this word array.
|
||||||
|
*/
|
||||||
|
var WordArray = C_lib.WordArray = Base.extend({
|
||||||
|
/**
|
||||||
|
* Initializes a newly created word array.
|
||||||
|
*
|
||||||
|
* @param {Array} words (Optional) An array of 32-bit words.
|
||||||
|
* @param {number} sigBytes (Optional) The number of significant bytes in the words.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var wordArray = CryptoJS.lib.WordArray.create();
|
||||||
|
* var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607]);
|
||||||
|
* var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607], 6);
|
||||||
|
*/
|
||||||
|
init: function (words, sigBytes) {
|
||||||
|
words = this.words = words || [];
|
||||||
|
|
||||||
|
if (sigBytes != undefined) {
|
||||||
|
this.sigBytes = sigBytes;
|
||||||
|
} else {
|
||||||
|
this.sigBytes = words.length * 4;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts this word array to a string.
|
||||||
|
*
|
||||||
|
* @param {Encoder} encoder (Optional) The encoding strategy to use. Default: CryptoJS.enc.Hex
|
||||||
|
*
|
||||||
|
* @return {string} The stringified word array.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var string = wordArray + '';
|
||||||
|
* var string = wordArray.toString();
|
||||||
|
* var string = wordArray.toString(CryptoJS.enc.Utf8);
|
||||||
|
*/
|
||||||
|
toString: function (encoder) {
|
||||||
|
return (encoder || Hex).stringify(this);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Concatenates a word array to this word array.
|
||||||
|
*
|
||||||
|
* @param {WordArray} wordArray The word array to append.
|
||||||
|
*
|
||||||
|
* @return {WordArray} This word array.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* wordArray1.concat(wordArray2);
|
||||||
|
*/
|
||||||
|
concat: function (wordArray) {
|
||||||
|
// Shortcuts
|
||||||
|
var thisWords = this.words;
|
||||||
|
var thatWords = wordArray.words;
|
||||||
|
var thisSigBytes = this.sigBytes;
|
||||||
|
var thatSigBytes = wordArray.sigBytes;
|
||||||
|
|
||||||
|
// Clamp excess bits
|
||||||
|
this.clamp();
|
||||||
|
|
||||||
|
// Concat
|
||||||
|
if (thisSigBytes % 4) {
|
||||||
|
// Copy one byte at a time
|
||||||
|
for (var i = 0; i < thatSigBytes; i++) {
|
||||||
|
var thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
||||||
|
thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Copy one word at a time
|
||||||
|
for (var i = 0; i < thatSigBytes; i += 4) {
|
||||||
|
thisWords[(thisSigBytes + i) >>> 2] = thatWords[i >>> 2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.sigBytes += thatSigBytes;
|
||||||
|
|
||||||
|
// Chainable
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes insignificant bits.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* wordArray.clamp();
|
||||||
|
*/
|
||||||
|
clamp: function () {
|
||||||
|
// Shortcuts
|
||||||
|
var words = this.words;
|
||||||
|
var sigBytes = this.sigBytes;
|
||||||
|
|
||||||
|
// Clamp
|
||||||
|
words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8);
|
||||||
|
words.length = Math.ceil(sigBytes / 4);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a copy of this word array.
|
||||||
|
*
|
||||||
|
* @return {WordArray} The clone.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var clone = wordArray.clone();
|
||||||
|
*/
|
||||||
|
clone: function () {
|
||||||
|
var clone = Base.clone.call(this);
|
||||||
|
clone.words = this.words.slice(0);
|
||||||
|
|
||||||
|
return clone;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a word array filled with random bytes.
|
||||||
|
*
|
||||||
|
* @param {number} nBytes The number of random bytes to generate.
|
||||||
|
*
|
||||||
|
* @return {WordArray} The random word array.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var wordArray = CryptoJS.lib.WordArray.random(16);
|
||||||
|
*/
|
||||||
|
random: function (nBytes) {
|
||||||
|
var words = [];
|
||||||
|
|
||||||
|
var r = (function (m_w) {
|
||||||
|
var m_w = m_w;
|
||||||
|
var m_z = 0x3ade68b1;
|
||||||
|
var mask = 0xffffffff;
|
||||||
|
|
||||||
|
return function () {
|
||||||
|
m_z = (0x9069 * (m_z & 0xFFFF) + (m_z >> 0x10)) & mask;
|
||||||
|
m_w = (0x4650 * (m_w & 0xFFFF) + (m_w >> 0x10)) & mask;
|
||||||
|
var result = ((m_z << 0x10) + m_w) & mask;
|
||||||
|
result /= 0x100000000;
|
||||||
|
result += 0.5;
|
||||||
|
return result * (Math.random() > .5 ? 1 : -1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (var i = 0, rcache; i < nBytes; i += 4) {
|
||||||
|
var _r = r((rcache || Math.random()) * 0x100000000);
|
||||||
|
|
||||||
|
rcache = _r() * 0x3ade67b7;
|
||||||
|
words.push((_r() * 0x100000000) | 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WordArray.init(words, nBytes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encoder namespace.
|
||||||
|
*/
|
||||||
|
var C_enc = C.enc = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hex encoding strategy.
|
||||||
|
*/
|
||||||
|
var Hex = C_enc.Hex = {
|
||||||
|
/**
|
||||||
|
* Converts a word array to a hex string.
|
||||||
|
*
|
||||||
|
* @param {WordArray} wordArray The word array.
|
||||||
|
*
|
||||||
|
* @return {string} The hex string.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var hexString = CryptoJS.enc.Hex.stringify(wordArray);
|
||||||
|
*/
|
||||||
|
stringify: function (wordArray) {
|
||||||
|
// Shortcuts
|
||||||
|
var words = wordArray.words;
|
||||||
|
var sigBytes = wordArray.sigBytes;
|
||||||
|
|
||||||
|
// Convert
|
||||||
|
var hexChars = [];
|
||||||
|
for (var i = 0; i < sigBytes; i++) {
|
||||||
|
var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
||||||
|
hexChars.push((bite >>> 4).toString(16));
|
||||||
|
hexChars.push((bite & 0x0f).toString(16));
|
||||||
|
}
|
||||||
|
|
||||||
|
return hexChars.join('');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a hex string to a word array.
|
||||||
|
*
|
||||||
|
* @param {string} hexStr The hex string.
|
||||||
|
*
|
||||||
|
* @return {WordArray} The word array.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var wordArray = CryptoJS.enc.Hex.parse(hexString);
|
||||||
|
*/
|
||||||
|
parse: function (hexStr) {
|
||||||
|
// Shortcut
|
||||||
|
var hexStrLength = hexStr.length;
|
||||||
|
|
||||||
|
// Convert
|
||||||
|
var words = [];
|
||||||
|
for (var i = 0; i < hexStrLength; i += 2) {
|
||||||
|
words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WordArray.init(words, hexStrLength / 2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Latin1 encoding strategy.
|
||||||
|
*/
|
||||||
|
var Latin1 = C_enc.Latin1 = {
|
||||||
|
/**
|
||||||
|
* Converts a word array to a Latin1 string.
|
||||||
|
*
|
||||||
|
* @param {WordArray} wordArray The word array.
|
||||||
|
*
|
||||||
|
* @return {string} The Latin1 string.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var latin1String = CryptoJS.enc.Latin1.stringify(wordArray);
|
||||||
|
*/
|
||||||
|
stringify: function (wordArray) {
|
||||||
|
// Shortcuts
|
||||||
|
var words = wordArray.words;
|
||||||
|
var sigBytes = wordArray.sigBytes;
|
||||||
|
|
||||||
|
// Convert
|
||||||
|
var latin1Chars = [];
|
||||||
|
for (var i = 0; i < sigBytes; i++) {
|
||||||
|
var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
||||||
|
latin1Chars.push(String.fromCharCode(bite));
|
||||||
|
}
|
||||||
|
|
||||||
|
return latin1Chars.join('');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Latin1 string to a word array.
|
||||||
|
*
|
||||||
|
* @param {string} latin1Str The Latin1 string.
|
||||||
|
*
|
||||||
|
* @return {WordArray} The word array.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var wordArray = CryptoJS.enc.Latin1.parse(latin1String);
|
||||||
|
*/
|
||||||
|
parse: function (latin1Str) {
|
||||||
|
// Shortcut
|
||||||
|
var latin1StrLength = latin1Str.length;
|
||||||
|
|
||||||
|
// Convert
|
||||||
|
var words = [];
|
||||||
|
for (var i = 0; i < latin1StrLength; i++) {
|
||||||
|
words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WordArray.init(words, latin1StrLength);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UTF-8 encoding strategy.
|
||||||
|
*/
|
||||||
|
var Utf8 = C_enc.Utf8 = {
|
||||||
|
/**
|
||||||
|
* Converts a word array to a UTF-8 string.
|
||||||
|
*
|
||||||
|
* @param {WordArray} wordArray The word array.
|
||||||
|
*
|
||||||
|
* @return {string} The UTF-8 string.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var utf8String = CryptoJS.enc.Utf8.stringify(wordArray);
|
||||||
|
*/
|
||||||
|
stringify: function (wordArray) {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(escape(Latin1.stringify(wordArray)));
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Malformed UTF-8 data');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a UTF-8 string to a word array.
|
||||||
|
*
|
||||||
|
* @param {string} utf8Str The UTF-8 string.
|
||||||
|
*
|
||||||
|
* @return {WordArray} The word array.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var wordArray = CryptoJS.enc.Utf8.parse(utf8String);
|
||||||
|
*/
|
||||||
|
parse: function (utf8Str) {
|
||||||
|
return Latin1.parse(unescape(encodeURIComponent(utf8Str)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract buffered block algorithm template.
|
||||||
|
*
|
||||||
|
* The property blockSize must be implemented in a concrete subtype.
|
||||||
|
*
|
||||||
|
* @property {number} _minBufferSize The number of blocks that should be kept unprocessed in the buffer. Default: 0
|
||||||
|
*/
|
||||||
|
var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm = Base.extend({
|
||||||
|
/**
|
||||||
|
* Resets this block algorithm's data buffer to its initial state.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* bufferedBlockAlgorithm.reset();
|
||||||
|
*/
|
||||||
|
reset: function () {
|
||||||
|
// Initial values
|
||||||
|
this._data = new WordArray.init();
|
||||||
|
this._nDataBytes = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds new data to this block algorithm's buffer.
|
||||||
|
*
|
||||||
|
* @param {WordArray|string} data The data to append. Strings are converted to a WordArray using UTF-8.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* bufferedBlockAlgorithm._append('data');
|
||||||
|
* bufferedBlockAlgorithm._append(wordArray);
|
||||||
|
*/
|
||||||
|
_append: function (data) {
|
||||||
|
// Convert string to WordArray, else assume WordArray already
|
||||||
|
if (typeof data == 'string') {
|
||||||
|
data = Utf8.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append
|
||||||
|
this._data.concat(data);
|
||||||
|
this._nDataBytes += data.sigBytes;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes available data blocks.
|
||||||
|
*
|
||||||
|
* This method invokes _doProcessBlock(offset), which must be implemented by a concrete subtype.
|
||||||
|
*
|
||||||
|
* @param {boolean} doFlush Whether all blocks and partial blocks should be processed.
|
||||||
|
*
|
||||||
|
* @return {WordArray} The processed data.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var processedData = bufferedBlockAlgorithm._process();
|
||||||
|
* var processedData = bufferedBlockAlgorithm._process(!!'flush');
|
||||||
|
*/
|
||||||
|
_process: function (doFlush) {
|
||||||
|
// Shortcuts
|
||||||
|
var data = this._data;
|
||||||
|
var dataWords = data.words;
|
||||||
|
var dataSigBytes = data.sigBytes;
|
||||||
|
var blockSize = this.blockSize;
|
||||||
|
var blockSizeBytes = blockSize * 4;
|
||||||
|
|
||||||
|
// Count blocks ready
|
||||||
|
var nBlocksReady = dataSigBytes / blockSizeBytes;
|
||||||
|
if (doFlush) {
|
||||||
|
// Round up to include partial blocks
|
||||||
|
nBlocksReady = Math.ceil(nBlocksReady);
|
||||||
|
} else {
|
||||||
|
// Round down to include only full blocks,
|
||||||
|
// less the number of blocks that must remain in the buffer
|
||||||
|
nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count words ready
|
||||||
|
var nWordsReady = nBlocksReady * blockSize;
|
||||||
|
|
||||||
|
// Count bytes ready
|
||||||
|
var nBytesReady = Math.min(nWordsReady * 4, dataSigBytes);
|
||||||
|
|
||||||
|
// Process blocks
|
||||||
|
if (nWordsReady) {
|
||||||
|
for (var offset = 0; offset < nWordsReady; offset += blockSize) {
|
||||||
|
// Perform concrete-algorithm logic
|
||||||
|
this._doProcessBlock(dataWords, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove processed words
|
||||||
|
var processedWords = dataWords.splice(0, nWordsReady);
|
||||||
|
data.sigBytes -= nBytesReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return processed words
|
||||||
|
return new WordArray.init(processedWords, nBytesReady);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a copy of this object.
|
||||||
|
*
|
||||||
|
* @return {Object} The clone.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var clone = bufferedBlockAlgorithm.clone();
|
||||||
|
*/
|
||||||
|
clone: function () {
|
||||||
|
var clone = Base.clone.call(this);
|
||||||
|
clone._data = this._data.clone();
|
||||||
|
|
||||||
|
return clone;
|
||||||
|
},
|
||||||
|
|
||||||
|
_minBufferSize: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract hasher template.
|
||||||
|
*
|
||||||
|
* @property {number} blockSize The number of 32-bit words this hasher operates on. Default: 16 (512 bits)
|
||||||
|
*/
|
||||||
|
var Hasher = C_lib.Hasher = BufferedBlockAlgorithm.extend({
|
||||||
|
/**
|
||||||
|
* Configuration options.
|
||||||
|
*/
|
||||||
|
cfg: Base.extend(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a newly created hasher.
|
||||||
|
*
|
||||||
|
* @param {Object} cfg (Optional) The configuration options to use for this hash computation.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var hasher = CryptoJS.algo.SHA256.create();
|
||||||
|
*/
|
||||||
|
init: function (cfg) {
|
||||||
|
// Apply config defaults
|
||||||
|
this.cfg = this.cfg.extend(cfg);
|
||||||
|
|
||||||
|
// Set initial values
|
||||||
|
this.reset();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets this hasher to its initial state.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* hasher.reset();
|
||||||
|
*/
|
||||||
|
reset: function () {
|
||||||
|
// Reset data buffer
|
||||||
|
BufferedBlockAlgorithm.reset.call(this);
|
||||||
|
|
||||||
|
// Perform concrete-hasher logic
|
||||||
|
this._doReset();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates this hasher with a message.
|
||||||
|
*
|
||||||
|
* @param {WordArray|string} messageUpdate The message to append.
|
||||||
|
*
|
||||||
|
* @return {Hasher} This hasher.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* hasher.update('message');
|
||||||
|
* hasher.update(wordArray);
|
||||||
|
*/
|
||||||
|
update: function (messageUpdate) {
|
||||||
|
// Append
|
||||||
|
this._append(messageUpdate);
|
||||||
|
|
||||||
|
// Update the hash
|
||||||
|
this._process();
|
||||||
|
|
||||||
|
// Chainable
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finalizes the hash computation.
|
||||||
|
* Note that the finalize operation is effectively a destructive, read-once operation.
|
||||||
|
*
|
||||||
|
* @param {WordArray|string} messageUpdate (Optional) A final message update.
|
||||||
|
*
|
||||||
|
* @return {WordArray} The hash.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var hash = hasher.finalize();
|
||||||
|
* var hash = hasher.finalize('message');
|
||||||
|
* var hash = hasher.finalize(wordArray);
|
||||||
|
*/
|
||||||
|
finalize: function (messageUpdate) {
|
||||||
|
// Final message update
|
||||||
|
if (messageUpdate) {
|
||||||
|
this._append(messageUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform concrete-hasher logic
|
||||||
|
var hash = this._doFinalize();
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
},
|
||||||
|
|
||||||
|
blockSize: 512/32,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a shortcut function to a hasher's object interface.
|
||||||
|
*
|
||||||
|
* @param {Hasher} hasher The hasher to create a helper for.
|
||||||
|
*
|
||||||
|
* @return {Function} The shortcut function.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var SHA256 = CryptoJS.lib.Hasher._createHelper(CryptoJS.algo.SHA256);
|
||||||
|
*/
|
||||||
|
_createHelper: function (hasher) {
|
||||||
|
return function (message, cfg) {
|
||||||
|
return new hasher.init(cfg).finalize(message);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a shortcut function to the HMAC's object interface.
|
||||||
|
*
|
||||||
|
* @param {Hasher} hasher The hasher to use in this HMAC helper.
|
||||||
|
*
|
||||||
|
* @return {Function} The shortcut function.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var HmacSHA256 = CryptoJS.lib.Hasher._createHmacHelper(CryptoJS.algo.SHA256);
|
||||||
|
*/
|
||||||
|
_createHmacHelper: function (hasher) {
|
||||||
|
return function (message, key) {
|
||||||
|
return new C_algo.HMAC.init(hasher, key).finalize(message);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Algorithm namespace.
|
||||||
|
*/
|
||||||
|
var C_algo = C.algo = {};
|
||||||
|
|
||||||
|
return C;
|
||||||
|
}(Math));
|
||||||
|
|
||||||
|
|
||||||
|
return CryptoJS;
|
||||||
|
|
||||||
|
}));
|
||||||
268
src/frontend/public/rnode-flasher/js/crypto-js@3.9.1-1/md5.js
Normal file
268
src/frontend/public/rnode-flasher/js/crypto-js@3.9.1-1/md5.js
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
;(function (root, factory) {
|
||||||
|
if (typeof exports === "object") {
|
||||||
|
// CommonJS
|
||||||
|
module.exports = exports = factory(require("./core"));
|
||||||
|
}
|
||||||
|
else if (typeof define === "function" && define.amd) {
|
||||||
|
// AMD
|
||||||
|
define(["./core"], factory);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Global (browser)
|
||||||
|
factory(root.CryptoJS);
|
||||||
|
}
|
||||||
|
}(this, function (CryptoJS) {
|
||||||
|
|
||||||
|
(function (Math) {
|
||||||
|
// Shortcuts
|
||||||
|
var C = CryptoJS;
|
||||||
|
var C_lib = C.lib;
|
||||||
|
var WordArray = C_lib.WordArray;
|
||||||
|
var Hasher = C_lib.Hasher;
|
||||||
|
var C_algo = C.algo;
|
||||||
|
|
||||||
|
// Constants table
|
||||||
|
var T = [];
|
||||||
|
|
||||||
|
// Compute constants
|
||||||
|
(function () {
|
||||||
|
for (var i = 0; i < 64; i++) {
|
||||||
|
T[i] = (Math.abs(Math.sin(i + 1)) * 0x100000000) | 0;
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MD5 hash algorithm.
|
||||||
|
*/
|
||||||
|
var MD5 = C_algo.MD5 = Hasher.extend({
|
||||||
|
_doReset: function () {
|
||||||
|
this._hash = new WordArray.init([
|
||||||
|
0x67452301, 0xefcdab89,
|
||||||
|
0x98badcfe, 0x10325476
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
_doProcessBlock: function (M, offset) {
|
||||||
|
// Swap endian
|
||||||
|
for (var i = 0; i < 16; i++) {
|
||||||
|
// Shortcuts
|
||||||
|
var offset_i = offset + i;
|
||||||
|
var M_offset_i = M[offset_i];
|
||||||
|
|
||||||
|
M[offset_i] = (
|
||||||
|
(((M_offset_i << 8) | (M_offset_i >>> 24)) & 0x00ff00ff) |
|
||||||
|
(((M_offset_i << 24) | (M_offset_i >>> 8)) & 0xff00ff00)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shortcuts
|
||||||
|
var H = this._hash.words;
|
||||||
|
|
||||||
|
var M_offset_0 = M[offset + 0];
|
||||||
|
var M_offset_1 = M[offset + 1];
|
||||||
|
var M_offset_2 = M[offset + 2];
|
||||||
|
var M_offset_3 = M[offset + 3];
|
||||||
|
var M_offset_4 = M[offset + 4];
|
||||||
|
var M_offset_5 = M[offset + 5];
|
||||||
|
var M_offset_6 = M[offset + 6];
|
||||||
|
var M_offset_7 = M[offset + 7];
|
||||||
|
var M_offset_8 = M[offset + 8];
|
||||||
|
var M_offset_9 = M[offset + 9];
|
||||||
|
var M_offset_10 = M[offset + 10];
|
||||||
|
var M_offset_11 = M[offset + 11];
|
||||||
|
var M_offset_12 = M[offset + 12];
|
||||||
|
var M_offset_13 = M[offset + 13];
|
||||||
|
var M_offset_14 = M[offset + 14];
|
||||||
|
var M_offset_15 = M[offset + 15];
|
||||||
|
|
||||||
|
// Working varialbes
|
||||||
|
var a = H[0];
|
||||||
|
var b = H[1];
|
||||||
|
var c = H[2];
|
||||||
|
var d = H[3];
|
||||||
|
|
||||||
|
// Computation
|
||||||
|
a = FF(a, b, c, d, M_offset_0, 7, T[0]);
|
||||||
|
d = FF(d, a, b, c, M_offset_1, 12, T[1]);
|
||||||
|
c = FF(c, d, a, b, M_offset_2, 17, T[2]);
|
||||||
|
b = FF(b, c, d, a, M_offset_3, 22, T[3]);
|
||||||
|
a = FF(a, b, c, d, M_offset_4, 7, T[4]);
|
||||||
|
d = FF(d, a, b, c, M_offset_5, 12, T[5]);
|
||||||
|
c = FF(c, d, a, b, M_offset_6, 17, T[6]);
|
||||||
|
b = FF(b, c, d, a, M_offset_7, 22, T[7]);
|
||||||
|
a = FF(a, b, c, d, M_offset_8, 7, T[8]);
|
||||||
|
d = FF(d, a, b, c, M_offset_9, 12, T[9]);
|
||||||
|
c = FF(c, d, a, b, M_offset_10, 17, T[10]);
|
||||||
|
b = FF(b, c, d, a, M_offset_11, 22, T[11]);
|
||||||
|
a = FF(a, b, c, d, M_offset_12, 7, T[12]);
|
||||||
|
d = FF(d, a, b, c, M_offset_13, 12, T[13]);
|
||||||
|
c = FF(c, d, a, b, M_offset_14, 17, T[14]);
|
||||||
|
b = FF(b, c, d, a, M_offset_15, 22, T[15]);
|
||||||
|
|
||||||
|
a = GG(a, b, c, d, M_offset_1, 5, T[16]);
|
||||||
|
d = GG(d, a, b, c, M_offset_6, 9, T[17]);
|
||||||
|
c = GG(c, d, a, b, M_offset_11, 14, T[18]);
|
||||||
|
b = GG(b, c, d, a, M_offset_0, 20, T[19]);
|
||||||
|
a = GG(a, b, c, d, M_offset_5, 5, T[20]);
|
||||||
|
d = GG(d, a, b, c, M_offset_10, 9, T[21]);
|
||||||
|
c = GG(c, d, a, b, M_offset_15, 14, T[22]);
|
||||||
|
b = GG(b, c, d, a, M_offset_4, 20, T[23]);
|
||||||
|
a = GG(a, b, c, d, M_offset_9, 5, T[24]);
|
||||||
|
d = GG(d, a, b, c, M_offset_14, 9, T[25]);
|
||||||
|
c = GG(c, d, a, b, M_offset_3, 14, T[26]);
|
||||||
|
b = GG(b, c, d, a, M_offset_8, 20, T[27]);
|
||||||
|
a = GG(a, b, c, d, M_offset_13, 5, T[28]);
|
||||||
|
d = GG(d, a, b, c, M_offset_2, 9, T[29]);
|
||||||
|
c = GG(c, d, a, b, M_offset_7, 14, T[30]);
|
||||||
|
b = GG(b, c, d, a, M_offset_12, 20, T[31]);
|
||||||
|
|
||||||
|
a = HH(a, b, c, d, M_offset_5, 4, T[32]);
|
||||||
|
d = HH(d, a, b, c, M_offset_8, 11, T[33]);
|
||||||
|
c = HH(c, d, a, b, M_offset_11, 16, T[34]);
|
||||||
|
b = HH(b, c, d, a, M_offset_14, 23, T[35]);
|
||||||
|
a = HH(a, b, c, d, M_offset_1, 4, T[36]);
|
||||||
|
d = HH(d, a, b, c, M_offset_4, 11, T[37]);
|
||||||
|
c = HH(c, d, a, b, M_offset_7, 16, T[38]);
|
||||||
|
b = HH(b, c, d, a, M_offset_10, 23, T[39]);
|
||||||
|
a = HH(a, b, c, d, M_offset_13, 4, T[40]);
|
||||||
|
d = HH(d, a, b, c, M_offset_0, 11, T[41]);
|
||||||
|
c = HH(c, d, a, b, M_offset_3, 16, T[42]);
|
||||||
|
b = HH(b, c, d, a, M_offset_6, 23, T[43]);
|
||||||
|
a = HH(a, b, c, d, M_offset_9, 4, T[44]);
|
||||||
|
d = HH(d, a, b, c, M_offset_12, 11, T[45]);
|
||||||
|
c = HH(c, d, a, b, M_offset_15, 16, T[46]);
|
||||||
|
b = HH(b, c, d, a, M_offset_2, 23, T[47]);
|
||||||
|
|
||||||
|
a = II(a, b, c, d, M_offset_0, 6, T[48]);
|
||||||
|
d = II(d, a, b, c, M_offset_7, 10, T[49]);
|
||||||
|
c = II(c, d, a, b, M_offset_14, 15, T[50]);
|
||||||
|
b = II(b, c, d, a, M_offset_5, 21, T[51]);
|
||||||
|
a = II(a, b, c, d, M_offset_12, 6, T[52]);
|
||||||
|
d = II(d, a, b, c, M_offset_3, 10, T[53]);
|
||||||
|
c = II(c, d, a, b, M_offset_10, 15, T[54]);
|
||||||
|
b = II(b, c, d, a, M_offset_1, 21, T[55]);
|
||||||
|
a = II(a, b, c, d, M_offset_8, 6, T[56]);
|
||||||
|
d = II(d, a, b, c, M_offset_15, 10, T[57]);
|
||||||
|
c = II(c, d, a, b, M_offset_6, 15, T[58]);
|
||||||
|
b = II(b, c, d, a, M_offset_13, 21, T[59]);
|
||||||
|
a = II(a, b, c, d, M_offset_4, 6, T[60]);
|
||||||
|
d = II(d, a, b, c, M_offset_11, 10, T[61]);
|
||||||
|
c = II(c, d, a, b, M_offset_2, 15, T[62]);
|
||||||
|
b = II(b, c, d, a, M_offset_9, 21, T[63]);
|
||||||
|
|
||||||
|
// Intermediate hash value
|
||||||
|
H[0] = (H[0] + a) | 0;
|
||||||
|
H[1] = (H[1] + b) | 0;
|
||||||
|
H[2] = (H[2] + c) | 0;
|
||||||
|
H[3] = (H[3] + d) | 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
_doFinalize: function () {
|
||||||
|
// Shortcuts
|
||||||
|
var data = this._data;
|
||||||
|
var dataWords = data.words;
|
||||||
|
|
||||||
|
var nBitsTotal = this._nDataBytes * 8;
|
||||||
|
var nBitsLeft = data.sigBytes * 8;
|
||||||
|
|
||||||
|
// Add padding
|
||||||
|
dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
|
||||||
|
|
||||||
|
var nBitsTotalH = Math.floor(nBitsTotal / 0x100000000);
|
||||||
|
var nBitsTotalL = nBitsTotal;
|
||||||
|
dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = (
|
||||||
|
(((nBitsTotalH << 8) | (nBitsTotalH >>> 24)) & 0x00ff00ff) |
|
||||||
|
(((nBitsTotalH << 24) | (nBitsTotalH >>> 8)) & 0xff00ff00)
|
||||||
|
);
|
||||||
|
dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = (
|
||||||
|
(((nBitsTotalL << 8) | (nBitsTotalL >>> 24)) & 0x00ff00ff) |
|
||||||
|
(((nBitsTotalL << 24) | (nBitsTotalL >>> 8)) & 0xff00ff00)
|
||||||
|
);
|
||||||
|
|
||||||
|
data.sigBytes = (dataWords.length + 1) * 4;
|
||||||
|
|
||||||
|
// Hash final blocks
|
||||||
|
this._process();
|
||||||
|
|
||||||
|
// Shortcuts
|
||||||
|
var hash = this._hash;
|
||||||
|
var H = hash.words;
|
||||||
|
|
||||||
|
// Swap endian
|
||||||
|
for (var i = 0; i < 4; i++) {
|
||||||
|
// Shortcut
|
||||||
|
var H_i = H[i];
|
||||||
|
|
||||||
|
H[i] = (((H_i << 8) | (H_i >>> 24)) & 0x00ff00ff) |
|
||||||
|
(((H_i << 24) | (H_i >>> 8)) & 0xff00ff00);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return final computed hash
|
||||||
|
return hash;
|
||||||
|
},
|
||||||
|
|
||||||
|
clone: function () {
|
||||||
|
var clone = Hasher.clone.call(this);
|
||||||
|
clone._hash = this._hash.clone();
|
||||||
|
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function FF(a, b, c, d, x, s, t) {
|
||||||
|
var n = a + ((b & c) | (~b & d)) + x + t;
|
||||||
|
return ((n << s) | (n >>> (32 - s))) + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function GG(a, b, c, d, x, s, t) {
|
||||||
|
var n = a + ((b & d) | (c & ~d)) + x + t;
|
||||||
|
return ((n << s) | (n >>> (32 - s))) + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HH(a, b, c, d, x, s, t) {
|
||||||
|
var n = a + (b ^ c ^ d) + x + t;
|
||||||
|
return ((n << s) | (n >>> (32 - s))) + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function II(a, b, c, d, x, s, t) {
|
||||||
|
var n = a + (c ^ (b | ~d)) + x + t;
|
||||||
|
return ((n << s) | (n >>> (32 - s))) + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortcut function to the hasher's object interface.
|
||||||
|
*
|
||||||
|
* @param {WordArray|string} message The message to hash.
|
||||||
|
*
|
||||||
|
* @return {WordArray} The hash.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var hash = CryptoJS.MD5('message');
|
||||||
|
* var hash = CryptoJS.MD5(wordArray);
|
||||||
|
*/
|
||||||
|
C.MD5 = Hasher._createHelper(MD5);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortcut function to the HMAC's object interface.
|
||||||
|
*
|
||||||
|
* @param {WordArray|string} message The message to hash.
|
||||||
|
* @param {WordArray|string} key The secret key.
|
||||||
|
*
|
||||||
|
* @return {WordArray} The HMAC.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* var hmac = CryptoJS.HmacMD5(message, key);
|
||||||
|
*/
|
||||||
|
C.HmacMD5 = Hasher._createHmacHelper(MD5);
|
||||||
|
}(Math));
|
||||||
|
|
||||||
|
|
||||||
|
return CryptoJS.MD5;
|
||||||
|
|
||||||
|
}));
|
||||||
File diff suppressed because one or more lines are too long
446
src/frontend/public/rnode-flasher/js/nrf52_dfu_flasher.js
Normal file
446
src/frontend/public/rnode-flasher/js/nrf52_dfu_flasher.js
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
/**
|
||||||
|
* A Web Serial based nRF52 flasher written by liam@liamcottle.com based on dfu_transport.serial.py
|
||||||
|
* https://github.com/adafruit/Adafruit_nRF52_nrfutil/blob/master/nordicsemi/dfu/dfu_transport_serial.py
|
||||||
|
*/
|
||||||
|
class Nrf52DfuFlasher {
|
||||||
|
|
||||||
|
DFU_TOUCH_BAUD = 1200;
|
||||||
|
SERIAL_PORT_OPEN_WAIT_TIME = 0.1;
|
||||||
|
TOUCH_RESET_WAIT_TIME = 1.5;
|
||||||
|
|
||||||
|
FLASH_BAUD = 115200;
|
||||||
|
|
||||||
|
HEX_TYPE_APPLICATION = 4;
|
||||||
|
|
||||||
|
DFU_INIT_PACKET = 1;
|
||||||
|
DFU_START_PACKET = 3;
|
||||||
|
DFU_DATA_PACKET = 4;
|
||||||
|
DFU_STOP_DATA_PACKET = 5;
|
||||||
|
|
||||||
|
DATA_INTEGRITY_CHECK_PRESENT = 1;
|
||||||
|
RELIABLE_PACKET = 1;
|
||||||
|
HCI_PACKET_TYPE = 14;
|
||||||
|
|
||||||
|
FLASH_PAGE_SIZE = 4096;
|
||||||
|
FLASH_PAGE_ERASE_TIME = 0.0897;
|
||||||
|
FLASH_WORD_WRITE_TIME = 0.000100;
|
||||||
|
FLASH_PAGE_WRITE_TIME = (this.FLASH_PAGE_SIZE/4) * this.FLASH_WORD_WRITE_TIME;
|
||||||
|
|
||||||
|
// The DFU packet max size
|
||||||
|
DFU_PACKET_MAX_SIZE = 512;
|
||||||
|
|
||||||
|
constructor(serialPort) {
|
||||||
|
this.serialPort = serialPort;
|
||||||
|
this.sequenceNumber = 0;
|
||||||
|
this.sd_size = 0;
|
||||||
|
this.total_size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for the provided milliseconds, and then resolves.
|
||||||
|
* @param millis
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async sleepMillis(millis) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, millis);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the provided data to the Serial Port.
|
||||||
|
* @param data
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async sendPacket(data) {
|
||||||
|
const writer = this.serialPort.writable.getWriter();
|
||||||
|
try {
|
||||||
|
await writer.write(new Uint8Array(data));
|
||||||
|
} finally {
|
||||||
|
writer.releaseLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Puts an nRF52 board into DFU mode by quickly opening and closing a serial port.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async enterDfuMode() {
|
||||||
|
|
||||||
|
// open port
|
||||||
|
await this.serialPort.open({
|
||||||
|
baudRate: this.DFU_TOUCH_BAUD,
|
||||||
|
});
|
||||||
|
|
||||||
|
// wait SERIAL_PORT_OPEN_WAIT_TIME before closing port
|
||||||
|
await this.sleepMillis(this.SERIAL_PORT_OPEN_WAIT_TIME * 1000);
|
||||||
|
|
||||||
|
// close port
|
||||||
|
await this.serialPort.close();
|
||||||
|
|
||||||
|
// wait TOUCH_RESET_WAIT_TIME for device to enter into DFU mode
|
||||||
|
await this.sleepMillis(this.TOUCH_RESET_WAIT_TIME * 1000);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flashes the provided firmware zip.
|
||||||
|
* @param firmwareZipBlob
|
||||||
|
* @param progressCallback
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async flash(firmwareZipBlob, progressCallback) {
|
||||||
|
|
||||||
|
// read zip file
|
||||||
|
const blobReader = new window.zip.BlobReader(firmwareZipBlob);
|
||||||
|
const zipReader = new window.zip.ZipReader(blobReader);
|
||||||
|
const zipEntries = await zipReader.getEntries();
|
||||||
|
|
||||||
|
// find manifest file
|
||||||
|
const manifestFile = zipEntries.find((zipEntry) => zipEntry.filename === "manifest.json");
|
||||||
|
if(!manifestFile){
|
||||||
|
throw "manifest.json not found in firmware file!";
|
||||||
|
}
|
||||||
|
|
||||||
|
// read manifest file as text
|
||||||
|
const text = await manifestFile.getData(new window.zip.TextWriter());
|
||||||
|
|
||||||
|
// parse manifest json
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
const manifest = json.manifest;
|
||||||
|
|
||||||
|
// todo softdevice_bootloader
|
||||||
|
// if self.manifest.softdevice_bootloader:
|
||||||
|
// self._dfu_send_image(HexType.SD_BL, self.manifest.softdevice_bootloader)
|
||||||
|
|
||||||
|
// todo softdevice
|
||||||
|
// if self.manifest.softdevice:
|
||||||
|
// self._dfu_send_image(HexType.SOFTDEVICE, self.manifest.softdevice)
|
||||||
|
|
||||||
|
// todo bootloader
|
||||||
|
// if self.manifest.bootloader:
|
||||||
|
// self._dfu_send_image(HexType.BOOTLOADER, self.manifest.bootloader)
|
||||||
|
|
||||||
|
// flash application image
|
||||||
|
if(manifest.application){
|
||||||
|
await this.dfuSendImage(this.HEX_TYPE_APPLICATION, zipEntries, manifest.application, progressCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the firmware image to the device in DFU mode.
|
||||||
|
* @param programMode
|
||||||
|
* @param zipEntries
|
||||||
|
* @param firmwareManifest
|
||||||
|
* @param progressCallback
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async dfuSendImage(programMode, zipEntries, firmwareManifest, progressCallback) {
|
||||||
|
|
||||||
|
// open port
|
||||||
|
await this.serialPort.open({
|
||||||
|
baudRate: this.FLASH_BAUD,
|
||||||
|
});
|
||||||
|
|
||||||
|
// wait SERIAL_PORT_OPEN_WAIT_TIME
|
||||||
|
await this.sleepMillis(this.SERIAL_PORT_OPEN_WAIT_TIME * 1000);
|
||||||
|
|
||||||
|
// file sizes
|
||||||
|
var softdeviceSize = 0
|
||||||
|
var bootloaderSize = 0
|
||||||
|
var applicationSize = 0
|
||||||
|
|
||||||
|
// read bin file (firmware)
|
||||||
|
const binFile = zipEntries.find((zipEntry) => zipEntry.filename === firmwareManifest.bin_file);
|
||||||
|
const firmware = await binFile.getData(new window.zip.Uint8ArrayWriter());
|
||||||
|
|
||||||
|
// read dat file (init packet)
|
||||||
|
const datFile = zipEntries.find((zipEntry) => zipEntry.filename === firmwareManifest.dat_file);
|
||||||
|
const init_packet = await datFile.getData(new window.zip.Uint8ArrayWriter());
|
||||||
|
|
||||||
|
// only support flashing application for now
|
||||||
|
if(programMode !== this.HEX_TYPE_APPLICATION){
|
||||||
|
throw "not implemented";
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine application size
|
||||||
|
if(programMode === this.HEX_TYPE_APPLICATION){
|
||||||
|
applicationSize = firmware.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Sending DFU start packet");
|
||||||
|
await this.sendStartDfu(programMode, softdeviceSize, bootloaderSize, applicationSize);
|
||||||
|
|
||||||
|
console.log("Sending DFU init packet");
|
||||||
|
await this.sendInitPacket(init_packet);
|
||||||
|
|
||||||
|
console.log("Sending firmware");
|
||||||
|
await this.sendFirmware(firmware, progressCallback);
|
||||||
|
|
||||||
|
// todo
|
||||||
|
// sleep(self.dfu_transport.get_activate_wait_time())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates CRC16 on the provided binaryData
|
||||||
|
* @param {Uint8Array} binaryData - Array with data to run CRC16 calculation on
|
||||||
|
* @param {number} crc - CRC value to start calculation with
|
||||||
|
* @return {number} - Calculated CRC value of binaryData
|
||||||
|
*/
|
||||||
|
calcCrc16(binaryData, crc = 0xffff) {
|
||||||
|
|
||||||
|
if(!(binaryData instanceof Uint8Array)){
|
||||||
|
throw new Error("calcCrc16 requires Uint8Array input");
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let b of binaryData){
|
||||||
|
crc = (crc >> 8 & 0x00FF) | (crc << 8 & 0xFF00);
|
||||||
|
crc ^= b;
|
||||||
|
crc ^= (crc & 0x00FF) >> 4;
|
||||||
|
crc ^= (crc << 8) << 4;
|
||||||
|
crc ^= ((crc & 0x00FF) << 4) << 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return crc & 0xFFFF;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode esc characters in a SLIP package.
|
||||||
|
* Replace 0xC0 with 0xDBDC and 0xDB with 0xDBDD.
|
||||||
|
* @param dataIn
|
||||||
|
* @returns {*[]}
|
||||||
|
*/
|
||||||
|
slipEncodeEscChars(dataIn) {
|
||||||
|
|
||||||
|
let result = [];
|
||||||
|
|
||||||
|
for(let i = 0; i < dataIn.length; i++){
|
||||||
|
let char = dataIn[i];
|
||||||
|
if(char === 0xC0){
|
||||||
|
result.push(0xDB);
|
||||||
|
result.push(0xDC);
|
||||||
|
} else if(char === 0xDB) {
|
||||||
|
result.push(0xDB);
|
||||||
|
result.push(0xDD);
|
||||||
|
} else {
|
||||||
|
result.push(char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an HCI packet from the provided frame data.
|
||||||
|
* https://github.com/adafruit/Adafruit_nRF52_nrfutil/blob/master/nordicsemi/dfu/dfu_transport_serial.py#L332
|
||||||
|
* @param frame
|
||||||
|
* @returns {*[]}
|
||||||
|
*/
|
||||||
|
createHciPacketFromFrame(frame) {
|
||||||
|
|
||||||
|
// increase sequence number, but roll over at 8
|
||||||
|
this.sequenceNumber = (this.sequenceNumber + 1) % 8;
|
||||||
|
|
||||||
|
// create slip header
|
||||||
|
const slipHeaderBytes = this.createSlipHeader(
|
||||||
|
this.sequenceNumber,
|
||||||
|
this.DATA_INTEGRITY_CHECK_PRESENT,
|
||||||
|
this.RELIABLE_PACKET,
|
||||||
|
this.HCI_PACKET_TYPE,
|
||||||
|
frame.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
// create packet data
|
||||||
|
let data = [
|
||||||
|
...slipHeaderBytes,
|
||||||
|
...frame,
|
||||||
|
];
|
||||||
|
|
||||||
|
// add crc of data
|
||||||
|
const crc = this.calcCrc16(new Uint8Array(data), 0xffff);
|
||||||
|
data.push(crc & 0xFF);
|
||||||
|
data.push((crc & 0xFF00) >> 8);
|
||||||
|
|
||||||
|
// add escape characters
|
||||||
|
return [
|
||||||
|
0xc0,
|
||||||
|
...this.slipEncodeEscChars(data),
|
||||||
|
0xc0,
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate how long we should wait for erasing data.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
getEraseWaitTime() {
|
||||||
|
// always wait at least 0.5 seconds
|
||||||
|
return Math.max(0.5, ((this.total_size / this.FLASH_PAGE_SIZE) + 1) * this.FLASH_PAGE_ERASE_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the image size packet sent in the DFU Start packet.
|
||||||
|
* @param softdeviceSize
|
||||||
|
* @param bootloaderSize
|
||||||
|
* @param appSize
|
||||||
|
* @returns {number[]}
|
||||||
|
*/
|
||||||
|
createImageSizePacket(softdeviceSize = 0, bootloaderSize = 0, appSize = 0) {
|
||||||
|
return [
|
||||||
|
...this.int32ToBytes(softdeviceSize),
|
||||||
|
...this.int32ToBytes(bootloaderSize),
|
||||||
|
...this.int32ToBytes(appSize),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the DFU Start packet to the device.
|
||||||
|
* @param mode
|
||||||
|
* @param softdevice_size
|
||||||
|
* @param bootloader_size
|
||||||
|
* @param app_size
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async sendStartDfu(mode, softdevice_size = 0, bootloader_size = 0, app_size = 0){
|
||||||
|
|
||||||
|
// create frame
|
||||||
|
const frame = [
|
||||||
|
...this.int32ToBytes(this.DFU_START_PACKET),
|
||||||
|
...this.int32ToBytes(mode),
|
||||||
|
...this.createImageSizePacket(softdevice_size, bootloader_size, app_size),
|
||||||
|
];
|
||||||
|
|
||||||
|
// send hci packet
|
||||||
|
await this.sendPacket(this.createHciPacketFromFrame(frame));
|
||||||
|
|
||||||
|
// remember file sizes for calculating erase wait time
|
||||||
|
this.sd_size = softdevice_size;
|
||||||
|
this.total_size = softdevice_size + bootloader_size + app_size;
|
||||||
|
|
||||||
|
// wait for initial erase
|
||||||
|
await this.sleepMillis(this.getEraseWaitTime() * 1000);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the DFU Init packet to the device.
|
||||||
|
* @param initPacket
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async sendInitPacket(initPacket){
|
||||||
|
|
||||||
|
// create frame
|
||||||
|
const frame = [
|
||||||
|
...this.int32ToBytes(this.DFU_INIT_PACKET),
|
||||||
|
...initPacket,
|
||||||
|
...this.int16ToBytes(0x0000), // padding required
|
||||||
|
];
|
||||||
|
|
||||||
|
// send hci packet
|
||||||
|
await this.sendPacket(this.createHciPacketFromFrame(frame));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the firmware file to the device in multiple chunks.
|
||||||
|
* @param firmware
|
||||||
|
* @param progressCallback
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async sendFirmware(firmware, progressCallback) {
|
||||||
|
|
||||||
|
const packets = [];
|
||||||
|
var packetsSent = 0;
|
||||||
|
|
||||||
|
// chunk firmware into separate packets
|
||||||
|
for(let i = 0; i < firmware.length; i += this.DFU_PACKET_MAX_SIZE){
|
||||||
|
packets.push(this.createHciPacketFromFrame([
|
||||||
|
...this.int32ToBytes(this.DFU_DATA_PACKET),
|
||||||
|
...firmware.slice(i, i + this.DFU_PACKET_MAX_SIZE),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// send initial progress
|
||||||
|
if(progressCallback){
|
||||||
|
progressCallback(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// send each packet one after the other
|
||||||
|
for(var i = 0; i < packets.length; i++){
|
||||||
|
|
||||||
|
// send packet
|
||||||
|
await this.sendPacket(packets[i]);
|
||||||
|
|
||||||
|
// wait a bit to allow device to write before sending next packet
|
||||||
|
await this.sleepMillis(this.FLASH_PAGE_WRITE_TIME * 1000);
|
||||||
|
|
||||||
|
// update progress
|
||||||
|
packetsSent++;
|
||||||
|
if(progressCallback){
|
||||||
|
const progress = Math.floor((packetsSent / packets.length) * 100);
|
||||||
|
progressCallback(progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// finished sending firmware, send DFU Stop Data packet
|
||||||
|
await this.sendPacket(this.createHciPacketFromFrame([
|
||||||
|
...this.int32ToBytes(this.DFU_STOP_DATA_PACKET),
|
||||||
|
]));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a SLIP header.
|
||||||
|
*
|
||||||
|
* For a description of the SLIP header go to:
|
||||||
|
* http://developer.nordicsemi.com/nRF51_SDK/doc/7.2.0/s110/html/a00093.html
|
||||||
|
*
|
||||||
|
* @param {number} seq - Packet sequence number
|
||||||
|
* @param {number} dip - Data integrity check
|
||||||
|
* @param {number} rp - Reliable packet
|
||||||
|
* @param {number} pktType - Payload packet
|
||||||
|
* @param {number} pktLen - Packet length
|
||||||
|
* @return {Uint8Array} - SLIP header
|
||||||
|
*/
|
||||||
|
createSlipHeader(seq, dip, rp, pktType, pktLen) {
|
||||||
|
let ints = [0, 0, 0, 0];
|
||||||
|
ints[0] = seq | (((seq + 1) % 8) << 3) | (dip << 6) | (rp << 7);
|
||||||
|
ints[1] = pktType | ((pktLen & 0x000F) << 4);
|
||||||
|
ints[2] = (pktLen & 0x0FF0) >> 4;
|
||||||
|
ints[3] = (~(ints[0] + ints[1] + ints[2]) + 1) & 0xFF;
|
||||||
|
return new Uint8Array(ints);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the provided int32 to 4 bytes.
|
||||||
|
* @param num
|
||||||
|
* @returns {number[]}
|
||||||
|
*/
|
||||||
|
int32ToBytes(num){
|
||||||
|
return [
|
||||||
|
(num & 0x000000ff),
|
||||||
|
(num & 0x0000ff00) >> 8,
|
||||||
|
(num & 0x00ff0000) >> 16,
|
||||||
|
(num & 0xff000000) >> 24,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the provided int16 to 2 bytes.
|
||||||
|
* @param num
|
||||||
|
* @returns {number[]}
|
||||||
|
*/
|
||||||
|
int16ToBytes(num){
|
||||||
|
return [
|
||||||
|
(num & 0x00FF),
|
||||||
|
(num & 0xFF00) >> 8,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
961
src/frontend/public/rnode-flasher/js/rnode.js
Normal file
961
src/frontend/public/rnode-flasher/js/rnode.js
Normal file
@@ -0,0 +1,961 @@
|
|||||||
|
class Utils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for the provided milliseconds, and then resolves.
|
||||||
|
* @param millis
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async sleepMillis(millis) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, millis);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static bytesToHex(bytes) {
|
||||||
|
for(var hex = [], i = 0; i < bytes.length; i++){
|
||||||
|
var current = bytes[i] < 0 ? bytes[i] + 256 : bytes[i];
|
||||||
|
hex.push((current >>> 4).toString(16));
|
||||||
|
hex.push((current & 0xF).toString(16));
|
||||||
|
}
|
||||||
|
return hex.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
static md5(data) {
|
||||||
|
var bytes = [];
|
||||||
|
const hash = CryptoJS.MD5(CryptoJS.enc.Hex.parse(this.bytesToHex(data)));
|
||||||
|
for(var i = 0; i < hash.sigBytes; i++){
|
||||||
|
bytes.push((hash.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
static packUInt32BE(value) {
|
||||||
|
const buffer = new ArrayBuffer(4);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
view.setUint32(0, value, false);
|
||||||
|
return new Uint8Array(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
static unpackUInt32BE(byteArray) {
|
||||||
|
const buffer = new Uint8Array(byteArray).buffer;
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
return view.getUint32(0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class RNode {
|
||||||
|
|
||||||
|
KISS_FEND = 0xC0;
|
||||||
|
KISS_FESC = 0xDB;
|
||||||
|
KISS_TFEND = 0xDC;
|
||||||
|
KISS_TFESC = 0xDD;
|
||||||
|
|
||||||
|
CMD_FREQUENCY = 0x01;
|
||||||
|
CMD_BANDWIDTH = 0x02;
|
||||||
|
CMD_TXPOWER = 0x03;
|
||||||
|
CMD_SF = 0x04;
|
||||||
|
CMD_CR = 0x05;
|
||||||
|
CMD_RADIO_STATE = 0x06;
|
||||||
|
|
||||||
|
CMD_STAT_RX = 0x21;
|
||||||
|
CMD_STAT_TX = 0x22
|
||||||
|
CMD_STAT_RSSI = 0x23;
|
||||||
|
CMD_STAT_SNR = 0x24;
|
||||||
|
|
||||||
|
CMD_BOARD = 0x47;
|
||||||
|
CMD_PLATFORM = 0x48;
|
||||||
|
CMD_MCU = 0x49;
|
||||||
|
CMD_RESET = 0x55;
|
||||||
|
CMD_RESET_BYTE = 0xF8;
|
||||||
|
CMD_DEV_HASH = 0x56;
|
||||||
|
CMD_FW_VERSION = 0x50;
|
||||||
|
CMD_ROM_READ = 0x51;
|
||||||
|
CMD_ROM_WRITE = 0x52;
|
||||||
|
CMD_CONF_SAVE = 0x53;
|
||||||
|
CMD_CONF_DELETE = 0x54;
|
||||||
|
CMD_FW_HASH = 0x58;
|
||||||
|
CMD_UNLOCK_ROM = 0x59;
|
||||||
|
ROM_UNLOCK_BYTE = 0xF8;
|
||||||
|
CMD_HASHES = 0x60;
|
||||||
|
CMD_FW_UPD = 0x61;
|
||||||
|
|
||||||
|
CMD_BT_CTRL = 0x46;
|
||||||
|
CMD_BT_PIN = 0x62;
|
||||||
|
|
||||||
|
CMD_DISP_READ = 0x66;
|
||||||
|
|
||||||
|
CMD_DETECT = 0x08;
|
||||||
|
DETECT_REQ = 0x73;
|
||||||
|
DETECT_RESP = 0x46;
|
||||||
|
|
||||||
|
RADIO_STATE_OFF = 0x00;
|
||||||
|
RADIO_STATE_ON = 0x01;
|
||||||
|
RADIO_STATE_ASK = 0xFF;
|
||||||
|
|
||||||
|
CMD_ERROR = 0x90
|
||||||
|
ERROR_INITRADIO = 0x01
|
||||||
|
ERROR_TXFAILED = 0x02
|
||||||
|
ERROR_EEPROM_LOCKED = 0x03
|
||||||
|
|
||||||
|
PLATFORM_AVR = 0x90;
|
||||||
|
PLATFORM_ESP32 = 0x80;
|
||||||
|
PLATFORM_NRF52 = 0x70;
|
||||||
|
|
||||||
|
MCU_1284P = 0x91;
|
||||||
|
MCU_2560 = 0x92;
|
||||||
|
MCU_ESP32 = 0x81;
|
||||||
|
MCU_NRF52 = 0x71;
|
||||||
|
|
||||||
|
BOARD_RNODE = 0x31;
|
||||||
|
BOARD_HMBRW = 0x32;
|
||||||
|
BOARD_TBEAM = 0x33;
|
||||||
|
BOARD_HUZZAH32 = 0x34;
|
||||||
|
BOARD_GENERIC_ESP32 = 0x35;
|
||||||
|
BOARD_LORA32_V2_0 = 0x36;
|
||||||
|
BOARD_LORA32_V2_1 = 0x37;
|
||||||
|
BOARD_RAK4631 = 0x51;
|
||||||
|
|
||||||
|
HASH_TYPE_TARGET_FIRMWARE = 0x01;
|
||||||
|
HASH_TYPE_FIRMWARE = 0x02;
|
||||||
|
|
||||||
|
constructor(serialPort) {
|
||||||
|
this.serialPort = serialPort;
|
||||||
|
this.readable = serialPort.readable;
|
||||||
|
this.writable = serialPort.writable;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async fromSerialPort(serialPort) {
|
||||||
|
|
||||||
|
// open port
|
||||||
|
await serialPort.open({
|
||||||
|
baudRate: 115200,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new RNode(serialPort);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
try {
|
||||||
|
await this.serialPort.close();
|
||||||
|
} catch(e) {
|
||||||
|
console.log("failed to close serial port, ignoring...", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async write(bytes) {
|
||||||
|
const writer = this.writable.getWriter();
|
||||||
|
try {
|
||||||
|
await writer.write(new Uint8Array(bytes));
|
||||||
|
} finally {
|
||||||
|
writer.releaseLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async readFromSerialPort(timeoutMillis) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
|
||||||
|
// create reader
|
||||||
|
const reader = this.readable.getReader();
|
||||||
|
|
||||||
|
// timeout after provided millis
|
||||||
|
if(timeoutMillis != null){
|
||||||
|
setTimeout(() => {
|
||||||
|
reader.releaseLock();
|
||||||
|
reject("timeout");
|
||||||
|
}, timeoutMillis);
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt to read kiss frame
|
||||||
|
try {
|
||||||
|
let buffer = [];
|
||||||
|
while(true){
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
if(done){
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(value){
|
||||||
|
for(let byte of value){
|
||||||
|
buffer.push(byte);
|
||||||
|
if(byte === this.KISS_FEND){
|
||||||
|
if(buffer.length > 1){
|
||||||
|
resolve(this.handleKISSFrame(buffer));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
buffer = [this.KISS_FEND]; // Start new frame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading from serial port: ', error);
|
||||||
|
} finally {
|
||||||
|
reader.releaseLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKISSFrame(frame) {
|
||||||
|
|
||||||
|
let data = [];
|
||||||
|
let escaping = false;
|
||||||
|
|
||||||
|
// Skip the initial 0xC0 and process the rest
|
||||||
|
for(let i = 1; i < frame.length; i++){
|
||||||
|
let byte = frame[i];
|
||||||
|
if (escaping) {
|
||||||
|
if (byte === this.KISS_TFEND) {
|
||||||
|
data.push(this.KISS_FEND);
|
||||||
|
} else if (byte === this.KISS_TFESC) {
|
||||||
|
data.push(this.KISS_FESC);
|
||||||
|
}
|
||||||
|
escaping = false;
|
||||||
|
} else {
|
||||||
|
if (byte === this.KISS_FESC) {
|
||||||
|
escaping = true;
|
||||||
|
} else if (byte === this.KISS_FEND) {
|
||||||
|
// Ignore the end frame delimiter
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
data.push(byte);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log('Received KISS frame data:', new Uint8Array(data));
|
||||||
|
return data;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
createKissFrame(data) {
|
||||||
|
let frame = [this.KISS_FEND];
|
||||||
|
for(let byte of data){
|
||||||
|
if(byte === this.KISS_FEND){
|
||||||
|
frame.push(this.KISS_FESC, this.KISS_TFEND);
|
||||||
|
} else if(byte === this.KISS_FESC){
|
||||||
|
frame.push(this.KISS_FESC, this.KISS_TFESC);
|
||||||
|
} else {
|
||||||
|
frame.push(byte);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frame.push(this.KISS_FEND);
|
||||||
|
return new Uint8Array(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendKissCommand(data) {
|
||||||
|
await this.write(this.createKissFrame(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
async reset() {
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_RESET,
|
||||||
|
this.CMD_RESET_BYTE,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async detect() {
|
||||||
|
|
||||||
|
// ask if device is rnode
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_DETECT,
|
||||||
|
this.DETECT_REQ,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// read response from device
|
||||||
|
const [ command, responseByte ] = await this.readFromSerialPort();
|
||||||
|
|
||||||
|
// device is an rnode if response is as expected
|
||||||
|
return command === this.CMD_DETECT && responseByte === this.DETECT_RESP;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFirmwareVersion() {
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_FW_VERSION,
|
||||||
|
0x00,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// read response from device
|
||||||
|
var [ command, majorVersion, minorVersion ] = await this.readFromSerialPort();
|
||||||
|
if(minorVersion.length === 1){
|
||||||
|
minorVersion = "0" + minorVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.23
|
||||||
|
return majorVersion + "." + minorVersion;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPlatform() {
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_PLATFORM,
|
||||||
|
0x00,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// read response from device
|
||||||
|
const [ command, platformByte ] = await this.readFromSerialPort();
|
||||||
|
return platformByte;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMcu() {
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_MCU,
|
||||||
|
0x00,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// read response from device
|
||||||
|
const [ command, mcuByte ] = await this.readFromSerialPort();
|
||||||
|
return mcuByte;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBoard() {
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_BOARD,
|
||||||
|
0x00,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// read response from device
|
||||||
|
const [ command, boardByte ] = await this.readFromSerialPort();
|
||||||
|
return boardByte;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDeviceHash() {
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_DEV_HASH,
|
||||||
|
0x01, // anything != 0x00
|
||||||
|
]);
|
||||||
|
|
||||||
|
// read response from device
|
||||||
|
const [ command, ...deviceHash ] = await this.readFromSerialPort();
|
||||||
|
return deviceHash;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTargetFirmwareHash() {
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_HASHES,
|
||||||
|
this.HASH_TYPE_TARGET_FIRMWARE,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// read response from device
|
||||||
|
const [ command, hashType, ...targetFirmwareHash ] = await this.readFromSerialPort();
|
||||||
|
return targetFirmwareHash;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFirmwareHash() {
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_HASHES,
|
||||||
|
this.HASH_TYPE_FIRMWARE,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// read response from device
|
||||||
|
const [ command, hashType, ...firmwareHash ] = await this.readFromSerialPort();
|
||||||
|
return firmwareHash;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRom() {
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_ROM_READ,
|
||||||
|
0x00,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// read response from device
|
||||||
|
const [ command, ...eepromBytes ] = await this.readFromSerialPort();
|
||||||
|
return eepromBytes;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFrequency() {
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_FREQUENCY,
|
||||||
|
// request frequency by sending zero as 4 bytes
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// read response from device
|
||||||
|
const [ command, ...frequencyBytes ] = await this.readFromSerialPort();
|
||||||
|
|
||||||
|
// convert 4 bytes to 32bit integer representing frequency in hertz
|
||||||
|
const frequencyInHz = frequencyBytes[0] << 24 | frequencyBytes[1] << 16 | frequencyBytes[2] << 8 | frequencyBytes[3];
|
||||||
|
return frequencyInHz;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBandwidth() {
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_BANDWIDTH,
|
||||||
|
// request bandwidth by sending zero as 4 bytes
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// read response from device
|
||||||
|
const [ command, ...bandwidthBytes ] = await this.readFromSerialPort();
|
||||||
|
|
||||||
|
// convert 4 bytes to 32bit integer representing bandwidth in hertz
|
||||||
|
const bandwidthInHz = bandwidthBytes[0] << 24 | bandwidthBytes[1] << 16 | bandwidthBytes[2] << 8 | bandwidthBytes[3];
|
||||||
|
return bandwidthInHz;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTxPower() {
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_TXPOWER,
|
||||||
|
0xFF, // request tx power
|
||||||
|
]);
|
||||||
|
|
||||||
|
// read response from device
|
||||||
|
const [ command, txPower ] = await this.readFromSerialPort();
|
||||||
|
|
||||||
|
return txPower;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSpreadingFactor() {
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_SF,
|
||||||
|
0xFF, // request spreading factor
|
||||||
|
]);
|
||||||
|
|
||||||
|
// read response from device
|
||||||
|
const [ command, spreadingFactor ] = await this.readFromSerialPort();
|
||||||
|
|
||||||
|
return spreadingFactor;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCodingRate() {
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_CR,
|
||||||
|
0xFF, // request coding rate
|
||||||
|
]);
|
||||||
|
|
||||||
|
// read response from device
|
||||||
|
const [ command, codingRate ] = await this.readFromSerialPort();
|
||||||
|
|
||||||
|
return codingRate;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRadioState() {
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_RADIO_STATE,
|
||||||
|
0xFF, // request radio state
|
||||||
|
]);
|
||||||
|
|
||||||
|
// read response from device
|
||||||
|
const [ command, radioState ] = await this.readFromSerialPort();
|
||||||
|
|
||||||
|
return radioState;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRxStat() {
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_STAT_RX,
|
||||||
|
0x00,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// read response from device
|
||||||
|
const [ command, ...statBytes ] = await this.readFromSerialPort();
|
||||||
|
|
||||||
|
// convert 4 bytes to 32bit integer
|
||||||
|
const stat = statBytes[0] << 24 | statBytes[1] << 16 | statBytes[2] << 8 | statBytes[3];
|
||||||
|
return stat;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTxStat() {
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_STAT_TX,
|
||||||
|
0x00,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// read response from device
|
||||||
|
const [ command, ...statBytes ] = await this.readFromSerialPort();
|
||||||
|
|
||||||
|
// convert 4 bytes to 32bit integer
|
||||||
|
const stat = statBytes[0] << 24 | statBytes[1] << 16 | statBytes[2] << 8 | statBytes[3];
|
||||||
|
return stat;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRssiStat() {
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_STAT_RSSI,
|
||||||
|
0x00,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// read response from device
|
||||||
|
const [ command, rssi ] = await this.readFromSerialPort();
|
||||||
|
|
||||||
|
return rssi;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async disableBluetooth() {
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_BT_CTRL,
|
||||||
|
0x00, // stop
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async enableBluetooth() {
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_BT_CTRL,
|
||||||
|
0x01, // start
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async startBluetoothPairing() {
|
||||||
|
|
||||||
|
// enable pairing
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_BT_CTRL,
|
||||||
|
0x02, // enable pairing
|
||||||
|
]);
|
||||||
|
|
||||||
|
// todo: listen for packets, pin will be available once user has initiated pairing from Android device
|
||||||
|
|
||||||
|
// // attempt to get bluetooth pairing pin
|
||||||
|
// try {
|
||||||
|
//
|
||||||
|
// // read response from device
|
||||||
|
// const [ command, ...pinBytes ] = await this.readFromSerialPort(5000);
|
||||||
|
// if(command !== this.CMD_BT_PIN){
|
||||||
|
// throw `unexpected command response: ${command}`;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // convert 4 bytes to 32bit integer
|
||||||
|
// const pin = pinBytes[0] << 24 | pinBytes[1] << 16 | pinBytes[2] << 8 | pinBytes[3];
|
||||||
|
//
|
||||||
|
// // todo: remove logs
|
||||||
|
// console.log(pinBytes);
|
||||||
|
// console.log(pin);
|
||||||
|
//
|
||||||
|
// // todo: convert to string
|
||||||
|
// return pin;
|
||||||
|
//
|
||||||
|
// } catch(error) {
|
||||||
|
// throw `failed to get bluetooth pin: ${error}`;
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async readDisplay() {
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_DISP_READ,
|
||||||
|
0x01,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// read response from device
|
||||||
|
const [ command, ...displayBuffer ] = await this.readFromSerialPort();
|
||||||
|
|
||||||
|
return displayBuffer;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async setFrequency(frequencyInHz) {
|
||||||
|
|
||||||
|
const c1 = frequencyInHz >> 24;
|
||||||
|
const c2 = frequencyInHz >> 16 & 0xFF;
|
||||||
|
const c3 = frequencyInHz >> 8 & 0xFF;
|
||||||
|
const c4 = frequencyInHz & 0xFF;
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_FREQUENCY,
|
||||||
|
c1,
|
||||||
|
c2,
|
||||||
|
c3,
|
||||||
|
c4,
|
||||||
|
]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async setBandwidth(bandwidthInHz) {
|
||||||
|
|
||||||
|
const c1 = bandwidthInHz >> 24;
|
||||||
|
const c2 = bandwidthInHz >> 16 & 0xFF;
|
||||||
|
const c3 = bandwidthInHz >> 8 & 0xFF;
|
||||||
|
const c4 = bandwidthInHz & 0xFF;
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_BANDWIDTH,
|
||||||
|
c1,
|
||||||
|
c2,
|
||||||
|
c3,
|
||||||
|
c4,
|
||||||
|
]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async setTxPower(db) {
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_TXPOWER,
|
||||||
|
db,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setSpreadingFactor(spreadingFactor) {
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_SF,
|
||||||
|
spreadingFactor,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCodingRate(codingRate) {
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_CR,
|
||||||
|
codingRate,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setRadioStateOn() {
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_RADIO_STATE,
|
||||||
|
this.RADIO_STATE_ON,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setRadioStateOff() {
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_RADIO_STATE,
|
||||||
|
this.RADIO_STATE_OFF,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// setTNCMode
|
||||||
|
async saveConfig() {
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_CONF_SAVE,
|
||||||
|
0x00,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// setNormalMode
|
||||||
|
async deleteConfig() {
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_CONF_DELETE,
|
||||||
|
0x00,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async indicateFirmwareUpdate() {
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_FW_UPD,
|
||||||
|
0x01,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setFirmwareHash(hash) {
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_FW_HASH,
|
||||||
|
...hash,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeRom(address, value) {
|
||||||
|
|
||||||
|
// write to rom
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_ROM_WRITE,
|
||||||
|
address,
|
||||||
|
value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// wait a bit to allow device to write to rom
|
||||||
|
await Utils.sleepMillis(85);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async wipeRom() {
|
||||||
|
|
||||||
|
await this.sendKissCommand([
|
||||||
|
this.CMD_UNLOCK_ROM,
|
||||||
|
this.ROM_UNLOCK_BYTE,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// wiping can take up to 30 seconds
|
||||||
|
await Utils.sleepMillis(30000);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRomAsObject() {
|
||||||
|
const rom = await this.getRom();
|
||||||
|
return new ROM(rom);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class ROM {
|
||||||
|
|
||||||
|
static PLATFORM_AVR = 0x90
|
||||||
|
static PLATFORM_ESP32 = 0x80
|
||||||
|
static PLATFORM_NRF52 = 0x70
|
||||||
|
|
||||||
|
static MCU_1284P = 0x91
|
||||||
|
static MCU_2560 = 0x92
|
||||||
|
static MCU_ESP32 = 0x81
|
||||||
|
static MCU_NRF52 = 0x71
|
||||||
|
|
||||||
|
static PRODUCT_RAK4631 = 0x10
|
||||||
|
static MODEL_11 = 0x11
|
||||||
|
static MODEL_12 = 0x12
|
||||||
|
|
||||||
|
static PRODUCT_RNODE = 0x03
|
||||||
|
static MODEL_A1 = 0xA1
|
||||||
|
static MODEL_A6 = 0xA6
|
||||||
|
static MODEL_A4 = 0xA4
|
||||||
|
static MODEL_A9 = 0xA9
|
||||||
|
static MODEL_A3 = 0xA3
|
||||||
|
static MODEL_A8 = 0xA8
|
||||||
|
static MODEL_A2 = 0xA2
|
||||||
|
static MODEL_A7 = 0xA7
|
||||||
|
static MODEL_A5 = 0xA5;
|
||||||
|
static MODEL_AA = 0xAA;
|
||||||
|
|
||||||
|
static PRODUCT_T32_10 = 0xB2
|
||||||
|
static MODEL_BA = 0xBA
|
||||||
|
static MODEL_BB = 0xBB
|
||||||
|
|
||||||
|
static PRODUCT_T32_20 = 0xB0
|
||||||
|
static MODEL_B3 = 0xB3
|
||||||
|
static MODEL_B8 = 0xB8
|
||||||
|
|
||||||
|
static PRODUCT_T32_21 = 0xB1
|
||||||
|
static MODEL_B4 = 0xB4
|
||||||
|
static MODEL_B9 = 0xB9
|
||||||
|
static MODEL_B4_TCXO = 0x04 // The TCXO model codes are only used here to select the
|
||||||
|
static MODEL_B9_TCXO = 0x09 // correct firmware, actual model codes in firmware is still 0xB4 and 0xB9.
|
||||||
|
|
||||||
|
static PRODUCT_H32_V2 = 0xC0
|
||||||
|
static MODEL_C4 = 0xC4
|
||||||
|
static MODEL_C9 = 0xC9
|
||||||
|
|
||||||
|
static PRODUCT_H32_V3 = 0xC1
|
||||||
|
static MODEL_C5 = 0xC5
|
||||||
|
static MODEL_CA = 0xCA
|
||||||
|
|
||||||
|
static PRODUCT_TBEAM = 0xE0
|
||||||
|
static MODEL_E4 = 0xE4
|
||||||
|
static MODEL_E9 = 0xE9
|
||||||
|
static MODEL_E3 = 0xE3
|
||||||
|
static MODEL_E8 = 0xE8
|
||||||
|
|
||||||
|
static PRODUCT_TBEAM_S_V1 = 0xEA;
|
||||||
|
static MODEL_DB = 0xDB
|
||||||
|
static MODEL_DC = 0xDC
|
||||||
|
|
||||||
|
static PRODUCT_TDECK = 0xD0;
|
||||||
|
static MODEL_D4 = 0xD4;
|
||||||
|
static MODEL_D9 = 0xD9;
|
||||||
|
|
||||||
|
static PRODUCT_TECHO = 0x15;
|
||||||
|
static MODEL_T4 = 0x16;
|
||||||
|
static MODEL_T9 = 0x17;
|
||||||
|
|
||||||
|
static PRODUCT_HMBRW = 0xF0
|
||||||
|
static MODEL_FF = 0xFF
|
||||||
|
static MODEL_FE = 0xFE
|
||||||
|
|
||||||
|
static ADDR_PRODUCT = 0x00
|
||||||
|
static ADDR_MODEL = 0x01
|
||||||
|
static ADDR_HW_REV = 0x02
|
||||||
|
static ADDR_SERIAL = 0x03
|
||||||
|
static ADDR_MADE = 0x07
|
||||||
|
static ADDR_CHKSUM = 0x0B
|
||||||
|
static ADDR_SIGNATURE = 0x1B
|
||||||
|
static ADDR_INFO_LOCK = 0x9B
|
||||||
|
static ADDR_CONF_SF = 0x9C
|
||||||
|
static ADDR_CONF_CR = 0x9D
|
||||||
|
static ADDR_CONF_TXP = 0x9E
|
||||||
|
static ADDR_CONF_BW = 0x9F
|
||||||
|
static ADDR_CONF_FREQ = 0xA3
|
||||||
|
static ADDR_CONF_OK = 0xA7
|
||||||
|
|
||||||
|
static INFO_LOCK_BYTE = 0x73
|
||||||
|
static CONF_OK_BYTE = 0x73
|
||||||
|
|
||||||
|
static BOARD_RNODE = 0x31
|
||||||
|
static BOARD_HMBRW = 0x32
|
||||||
|
static BOARD_TBEAM = 0x33
|
||||||
|
static BOARD_HUZZAH32 = 0x34
|
||||||
|
static BOARD_GENERIC_ESP32 = 0x35
|
||||||
|
static BOARD_LORA32_V2_0 = 0x36
|
||||||
|
static BOARD_LORA32_V2_1 = 0x37
|
||||||
|
static BOARD_RAK4631 = 0x51
|
||||||
|
|
||||||
|
static MANUAL_FLASH_MODELS = [ROM.MODEL_A1, ROM.MODEL_A6]
|
||||||
|
|
||||||
|
constructor(eeprom) {
|
||||||
|
this.eeprom = eeprom;
|
||||||
|
}
|
||||||
|
|
||||||
|
getProduct() {
|
||||||
|
return this.eeprom[ROM.ADDR_PRODUCT];
|
||||||
|
}
|
||||||
|
|
||||||
|
getModel() {
|
||||||
|
return this.eeprom[ROM.ADDR_MODEL];
|
||||||
|
}
|
||||||
|
|
||||||
|
getHardwareRevision() {
|
||||||
|
return this.eeprom[ROM.ADDR_HW_REV];
|
||||||
|
}
|
||||||
|
|
||||||
|
getSerialNumber() {
|
||||||
|
return [
|
||||||
|
this.eeprom[ROM.ADDR_SERIAL],
|
||||||
|
this.eeprom[ROM.ADDR_SERIAL + 1],
|
||||||
|
this.eeprom[ROM.ADDR_SERIAL + 2],
|
||||||
|
this.eeprom[ROM.ADDR_SERIAL + 3],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
getMade() {
|
||||||
|
return [
|
||||||
|
this.eeprom[ROM.ADDR_MADE],
|
||||||
|
this.eeprom[ROM.ADDR_MADE + 1],
|
||||||
|
this.eeprom[ROM.ADDR_MADE + 2],
|
||||||
|
this.eeprom[ROM.ADDR_MADE + 3],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
getChecksum() {
|
||||||
|
const checksum = [];
|
||||||
|
for(var i = 0; i < 16; i++){
|
||||||
|
checksum.push(this.eeprom[ROM.ADDR_CHKSUM + i]);
|
||||||
|
}
|
||||||
|
return checksum;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSignature() {
|
||||||
|
const signature = [];
|
||||||
|
for(var i = 0; i < 128; i++){
|
||||||
|
signature.push(this.eeprom[ROM.ADDR_SIGNATURE + i]);
|
||||||
|
}
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCalculatedChecksum() {
|
||||||
|
return Utils.md5([
|
||||||
|
this.getProduct(),
|
||||||
|
this.getModel(),
|
||||||
|
this.getHardwareRevision(),
|
||||||
|
...this.getSerialNumber(),
|
||||||
|
...this.getMade(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfiguredSpreadingFactor() {
|
||||||
|
return this.eeprom[ROM.ADDR_CONF_SF];
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfiguredCodingRate() {
|
||||||
|
return this.eeprom[ROM.ADDR_CONF_CR];
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfiguredTxPower() {
|
||||||
|
return this.eeprom[ROM.ADDR_CONF_TXP];
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfiguredFrequency() {
|
||||||
|
return this.eeprom[ROM.ADDR_CONF_FREQ] << 24
|
||||||
|
| this.eeprom[ROM.ADDR_CONF_FREQ + 1] << 16
|
||||||
|
| this.eeprom[ROM.ADDR_CONF_FREQ + 2] << 8
|
||||||
|
| this.eeprom[ROM.ADDR_CONF_FREQ + 3];
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfiguredBandwidth() {
|
||||||
|
return this.eeprom[ROM.ADDR_CONF_BW] << 24
|
||||||
|
| this.eeprom[ROM.ADDR_CONF_BW + 1] << 16
|
||||||
|
| this.eeprom[ROM.ADDR_CONF_BW + 2] << 8
|
||||||
|
| this.eeprom[ROM.ADDR_CONF_BW + 3];
|
||||||
|
}
|
||||||
|
|
||||||
|
isInfoLocked() {
|
||||||
|
return this.eeprom[ROM.ADDR_INFO_LOCK] === ROM.INFO_LOCK_BYTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
isConfigured() {
|
||||||
|
return this.eeprom[ROM.ADDR_CONF_OK] === ROM.CONF_OK_BYTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
parse() {
|
||||||
|
|
||||||
|
// ensure info lock byte is set
|
||||||
|
if(!this.isInfoLocked()){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert to hex
|
||||||
|
const checksumHex = Utils.bytesToHex(this.getChecksum());
|
||||||
|
const calculatedChecksumHex = Utils.bytesToHex(this.getCalculatedChecksum());
|
||||||
|
const signatureHex = Utils.bytesToHex(this.getSignature());
|
||||||
|
|
||||||
|
// add details
|
||||||
|
var details = {
|
||||||
|
is_provisioned: true,
|
||||||
|
is_configured: this.isConfigured(),
|
||||||
|
product: this.getProduct(),
|
||||||
|
model: this.getModel(),
|
||||||
|
hardware_revision: this.getHardwareRevision(),
|
||||||
|
serial_number: Utils.unpackUInt32BE(this.getSerialNumber()),
|
||||||
|
made: Utils.unpackUInt32BE(this.getMade()),
|
||||||
|
checksum: checksumHex,
|
||||||
|
calculated_checksum: calculatedChecksumHex,
|
||||||
|
signature: signatureHex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// if configured, add configuration to details
|
||||||
|
if(details.is_configured){
|
||||||
|
details = {
|
||||||
|
...details,
|
||||||
|
configured_spreading_factor: this.getConfiguredSpreadingFactor(),
|
||||||
|
configured_coding_rate: this.getConfiguredCodingRate(),
|
||||||
|
configured_tx_power: this.getConfiguredTxPower(),
|
||||||
|
configured_frequency: this.getConfiguredFrequency(),
|
||||||
|
configured_bandwidth: this.getConfiguredBandwidth(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// if checksum in eeprom does not match checksum calculated from info, it is not provisioned
|
||||||
|
if(details.checksum !== details.calculated_checksum){
|
||||||
|
details.is_provisioned = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return details;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
16725
src/frontend/public/rnode-flasher/js/vue@3.4.26/dist/vue.global.js
vendored
Normal file
16725
src/frontend/public/rnode-flasher/js/vue@3.4.26/dist/vue.global.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
491
src/frontend/public/rnode-flasher/js/web-serial-polyfill@1.0.15/dist/serial.js
vendored
Normal file
491
src/frontend/public/rnode-flasher/js/web-serial-polyfill@1.0.15/dist/serial.js
vendored
Normal file
@@ -0,0 +1,491 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Google LLC
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* https://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.
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
export var SerialPolyfillProtocol;
|
||||||
|
(function (SerialPolyfillProtocol) {
|
||||||
|
SerialPolyfillProtocol[SerialPolyfillProtocol["UsbCdcAcm"] = 0] = "UsbCdcAcm";
|
||||||
|
})(SerialPolyfillProtocol || (SerialPolyfillProtocol = {}));
|
||||||
|
const kSetLineCoding = 0x20;
|
||||||
|
const kSetControlLineState = 0x22;
|
||||||
|
const kSendBreak = 0x23;
|
||||||
|
const kDefaultBufferSize = 255;
|
||||||
|
const kDefaultDataBits = 8;
|
||||||
|
const kDefaultParity = 'none';
|
||||||
|
const kDefaultStopBits = 1;
|
||||||
|
const kAcceptableDataBits = [16, 8, 7, 6, 5];
|
||||||
|
const kAcceptableStopBits = [1, 2];
|
||||||
|
const kAcceptableParity = ['none', 'even', 'odd'];
|
||||||
|
const kParityIndexMapping = ['none', 'odd', 'even'];
|
||||||
|
const kStopBitsIndexMapping = [1, 1.5, 2];
|
||||||
|
const kDefaultPolyfillOptions = {
|
||||||
|
protocol: SerialPolyfillProtocol.UsbCdcAcm,
|
||||||
|
usbControlInterfaceClass: 2,
|
||||||
|
usbTransferInterfaceClass: 10,
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Utility function to get the interface implementing a desired class.
|
||||||
|
* @param {USBDevice} device The USB device.
|
||||||
|
* @param {number} classCode The desired interface class.
|
||||||
|
* @return {USBInterface} The first interface found that implements the desired
|
||||||
|
* class.
|
||||||
|
* @throws TypeError if no interface is found.
|
||||||
|
*/
|
||||||
|
function findInterface(device, classCode) {
|
||||||
|
const configuration = device.configurations[0];
|
||||||
|
for (const iface of configuration.interfaces) {
|
||||||
|
const alternate = iface.alternates[0];
|
||||||
|
if (alternate.interfaceClass === classCode) {
|
||||||
|
return iface;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new TypeError(`Unable to find interface with class ${classCode}.`);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Utility function to get an endpoint with a particular direction.
|
||||||
|
* @param {USBInterface} iface The interface to search.
|
||||||
|
* @param {USBDirection} direction The desired transfer direction.
|
||||||
|
* @return {USBEndpoint} The first endpoint with the desired transfer direction.
|
||||||
|
* @throws TypeError if no endpoint is found.
|
||||||
|
*/
|
||||||
|
function findEndpoint(iface, direction) {
|
||||||
|
const alternate = iface.alternates[0];
|
||||||
|
for (const endpoint of alternate.endpoints) {
|
||||||
|
if (endpoint.direction == direction) {
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new TypeError(`Interface ${iface.interfaceNumber} does not have an ` +
|
||||||
|
`${direction} endpoint.`);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Implementation of the underlying source API[1] which reads data from a USB
|
||||||
|
* endpoint. This can be used to construct a ReadableStream.
|
||||||
|
*
|
||||||
|
* [1]: https://streams.spec.whatwg.org/#underlying-source-api
|
||||||
|
*/
|
||||||
|
class UsbEndpointUnderlyingSource {
|
||||||
|
/**
|
||||||
|
* Constructs a new UnderlyingSource that will pull data from the specified
|
||||||
|
* endpoint on the given USB device.
|
||||||
|
*
|
||||||
|
* @param {USBDevice} device
|
||||||
|
* @param {USBEndpoint} endpoint
|
||||||
|
* @param {function} onError function to be called on error
|
||||||
|
*/
|
||||||
|
constructor(device, endpoint, onError) {
|
||||||
|
this.type = 'bytes';
|
||||||
|
this.device_ = device;
|
||||||
|
this.endpoint_ = endpoint;
|
||||||
|
this.onError_ = onError;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Reads a chunk of data from the device.
|
||||||
|
*
|
||||||
|
* @param {ReadableByteStreamController} controller
|
||||||
|
*/
|
||||||
|
pull(controller) {
|
||||||
|
(async () => {
|
||||||
|
var _a;
|
||||||
|
let chunkSize;
|
||||||
|
if (controller.desiredSize) {
|
||||||
|
const d = controller.desiredSize / this.endpoint_.packetSize;
|
||||||
|
chunkSize = Math.ceil(d) * this.endpoint_.packetSize;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
chunkSize = this.endpoint_.packetSize;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await this.device_.transferIn(this.endpoint_.endpointNumber, chunkSize);
|
||||||
|
if (result.status != 'ok') {
|
||||||
|
controller.error(`USB error: ${result.status}`);
|
||||||
|
this.onError_();
|
||||||
|
}
|
||||||
|
if ((_a = result.data) === null || _a === void 0 ? void 0 : _a.buffer) {
|
||||||
|
const chunk = new Uint8Array(result.data.buffer, result.data.byteOffset, result.data.byteLength);
|
||||||
|
controller.enqueue(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
controller.error(error.toString());
|
||||||
|
this.onError_();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Implementation of the underlying sink API[2] which writes data to a USB
|
||||||
|
* endpoint. This can be used to construct a WritableStream.
|
||||||
|
*
|
||||||
|
* [2]: https://streams.spec.whatwg.org/#underlying-sink-api
|
||||||
|
*/
|
||||||
|
class UsbEndpointUnderlyingSink {
|
||||||
|
/**
|
||||||
|
* Constructs a new UnderlyingSink that will write data to the specified
|
||||||
|
* endpoint on the given USB device.
|
||||||
|
*
|
||||||
|
* @param {USBDevice} device
|
||||||
|
* @param {USBEndpoint} endpoint
|
||||||
|
* @param {function} onError function to be called on error
|
||||||
|
*/
|
||||||
|
constructor(device, endpoint, onError) {
|
||||||
|
this.device_ = device;
|
||||||
|
this.endpoint_ = endpoint;
|
||||||
|
this.onError_ = onError;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Writes a chunk to the device.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} chunk
|
||||||
|
* @param {WritableStreamDefaultController} controller
|
||||||
|
*/
|
||||||
|
async write(chunk, controller) {
|
||||||
|
try {
|
||||||
|
const result = await this.device_.transferOut(this.endpoint_.endpointNumber, chunk);
|
||||||
|
if (result.status != 'ok') {
|
||||||
|
controller.error(result.status);
|
||||||
|
this.onError_();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
controller.error(error.toString());
|
||||||
|
this.onError_();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** a class used to control serial devices over WebUSB */
|
||||||
|
export class SerialPort {
|
||||||
|
/**
|
||||||
|
* constructor taking a WebUSB device that creates a SerialPort instance.
|
||||||
|
* @param {USBDevice} device A device acquired from the WebUSB API
|
||||||
|
* @param {SerialPolyfillOptions} polyfillOptions Optional options to
|
||||||
|
* configure the polyfill.
|
||||||
|
*/
|
||||||
|
constructor(device, polyfillOptions) {
|
||||||
|
this.polyfillOptions_ = Object.assign(Object.assign({}, kDefaultPolyfillOptions), polyfillOptions);
|
||||||
|
this.outputSignals_ = {
|
||||||
|
dataTerminalReady: false,
|
||||||
|
requestToSend: false,
|
||||||
|
break: false,
|
||||||
|
};
|
||||||
|
this.device_ = device;
|
||||||
|
this.controlInterface_ = findInterface(this.device_, this.polyfillOptions_.usbControlInterfaceClass);
|
||||||
|
this.transferInterface_ = findInterface(this.device_, this.polyfillOptions_.usbTransferInterfaceClass);
|
||||||
|
this.inEndpoint_ = findEndpoint(this.transferInterface_, 'in');
|
||||||
|
this.outEndpoint_ = findEndpoint(this.transferInterface_, 'out');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Getter for the readable attribute. Constructs a new ReadableStream as
|
||||||
|
* necessary.
|
||||||
|
* @return {ReadableStream} the current readable stream
|
||||||
|
*/
|
||||||
|
get readable() {
|
||||||
|
var _a;
|
||||||
|
if (!this.readable_ && this.device_.opened) {
|
||||||
|
this.readable_ = new ReadableStream(new UsbEndpointUnderlyingSource(this.device_, this.inEndpoint_, () => {
|
||||||
|
this.readable_ = null;
|
||||||
|
}), {
|
||||||
|
highWaterMark: (_a = this.serialOptions_.bufferSize) !== null && _a !== void 0 ? _a : kDefaultBufferSize,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.readable_;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Getter for the writable attribute. Constructs a new WritableStream as
|
||||||
|
* necessary.
|
||||||
|
* @return {WritableStream} the current writable stream
|
||||||
|
*/
|
||||||
|
get writable() {
|
||||||
|
var _a;
|
||||||
|
if (!this.writable_ && this.device_.opened) {
|
||||||
|
this.writable_ = new WritableStream(new UsbEndpointUnderlyingSink(this.device_, this.outEndpoint_, () => {
|
||||||
|
this.writable_ = null;
|
||||||
|
}), new ByteLengthQueuingStrategy({
|
||||||
|
highWaterMark: (_a = this.serialOptions_.bufferSize) !== null && _a !== void 0 ? _a : kDefaultBufferSize,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return this.writable_;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* a function that opens the device and claims all interfaces needed to
|
||||||
|
* control and communicate to and from the serial device
|
||||||
|
* @param {SerialOptions} options Object containing serial options
|
||||||
|
* @return {Promise<void>} A promise that will resolve when device is ready
|
||||||
|
* for communication
|
||||||
|
*/
|
||||||
|
async open(options) {
|
||||||
|
this.serialOptions_ = options;
|
||||||
|
this.validateOptions();
|
||||||
|
try {
|
||||||
|
await this.device_.open();
|
||||||
|
if (this.device_.configuration === null) {
|
||||||
|
await this.device_.selectConfiguration(1);
|
||||||
|
}
|
||||||
|
await this.device_.claimInterface(this.controlInterface_.interfaceNumber);
|
||||||
|
if (this.controlInterface_ !== this.transferInterface_) {
|
||||||
|
await this.device_.claimInterface(this.transferInterface_.interfaceNumber);
|
||||||
|
}
|
||||||
|
await this.setLineCoding();
|
||||||
|
await this.setSignals({ dataTerminalReady: true });
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if (this.device_.opened) {
|
||||||
|
await this.device_.close();
|
||||||
|
}
|
||||||
|
throw new Error('Error setting up device: ' + error.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Closes the port.
|
||||||
|
*
|
||||||
|
* @return {Promise<void>} A promise that will resolve when the port is
|
||||||
|
* closed.
|
||||||
|
*/
|
||||||
|
async close() {
|
||||||
|
const promises = [];
|
||||||
|
if (this.readable_) {
|
||||||
|
promises.push(this.readable_.cancel());
|
||||||
|
}
|
||||||
|
if (this.writable_) {
|
||||||
|
promises.push(this.writable_.abort());
|
||||||
|
}
|
||||||
|
await Promise.all(promises);
|
||||||
|
this.readable_ = null;
|
||||||
|
this.writable_ = null;
|
||||||
|
if (this.device_.opened) {
|
||||||
|
await this.setSignals({ dataTerminalReady: false, requestToSend: false });
|
||||||
|
await this.device_.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Forgets the port.
|
||||||
|
*
|
||||||
|
* @return {Promise<void>} A promise that will resolve when the port is
|
||||||
|
* forgotten.
|
||||||
|
*/
|
||||||
|
async forget() {
|
||||||
|
return this.device_.forget();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* A function that returns properties of the device.
|
||||||
|
* @return {SerialPortInfo} Device properties.
|
||||||
|
*/
|
||||||
|
getInfo() {
|
||||||
|
return {
|
||||||
|
usbVendorId: this.device_.vendorId,
|
||||||
|
usbProductId: this.device_.productId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* A function used to change the serial settings of the device
|
||||||
|
* @param {object} options the object which carries serial settings data
|
||||||
|
* @return {Promise<void>} A promise that will resolve when the options are
|
||||||
|
* set
|
||||||
|
*/
|
||||||
|
reconfigure(options) {
|
||||||
|
this.serialOptions_ = Object.assign(Object.assign({}, this.serialOptions_), options);
|
||||||
|
this.validateOptions();
|
||||||
|
return this.setLineCoding();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets control signal state for the port.
|
||||||
|
* @param {SerialOutputSignals} signals The signals to enable or disable.
|
||||||
|
* @return {Promise<void>} a promise that is resolved when the signal state
|
||||||
|
* has been changed.
|
||||||
|
*/
|
||||||
|
async setSignals(signals) {
|
||||||
|
this.outputSignals_ = Object.assign(Object.assign({}, this.outputSignals_), signals);
|
||||||
|
if (signals.dataTerminalReady !== undefined ||
|
||||||
|
signals.requestToSend !== undefined) {
|
||||||
|
// The Set_Control_Line_State command expects a bitmap containing the
|
||||||
|
// values of all output signals that should be enabled or disabled.
|
||||||
|
//
|
||||||
|
// Ref: USB CDC specification version 1.1 §6.2.14.
|
||||||
|
const value = (this.outputSignals_.dataTerminalReady ? 1 << 0 : 0) |
|
||||||
|
(this.outputSignals_.requestToSend ? 1 << 1 : 0);
|
||||||
|
await this.device_.controlTransferOut({
|
||||||
|
'requestType': 'class',
|
||||||
|
'recipient': 'interface',
|
||||||
|
'request': kSetControlLineState,
|
||||||
|
'value': value,
|
||||||
|
'index': this.controlInterface_.interfaceNumber,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (signals.break !== undefined) {
|
||||||
|
// The SendBreak command expects to be given a duration for how long the
|
||||||
|
// break signal should be asserted. Passing 0xFFFF enables the signal
|
||||||
|
// until 0x0000 is send.
|
||||||
|
//
|
||||||
|
// Ref: USB CDC specification version 1.1 §6.2.15.
|
||||||
|
const value = this.outputSignals_.break ? 0xFFFF : 0x0000;
|
||||||
|
await this.device_.controlTransferOut({
|
||||||
|
'requestType': 'class',
|
||||||
|
'recipient': 'interface',
|
||||||
|
'request': kSendBreak,
|
||||||
|
'value': value,
|
||||||
|
'index': this.controlInterface_.interfaceNumber,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Checks the serial options for validity and throws an error if it is
|
||||||
|
* not valid
|
||||||
|
*/
|
||||||
|
validateOptions() {
|
||||||
|
if (!this.isValidBaudRate(this.serialOptions_.baudRate)) {
|
||||||
|
throw new RangeError('invalid Baud Rate ' + this.serialOptions_.baudRate);
|
||||||
|
}
|
||||||
|
if (!this.isValidDataBits(this.serialOptions_.dataBits)) {
|
||||||
|
throw new RangeError('invalid dataBits ' + this.serialOptions_.dataBits);
|
||||||
|
}
|
||||||
|
if (!this.isValidStopBits(this.serialOptions_.stopBits)) {
|
||||||
|
throw new RangeError('invalid stopBits ' + this.serialOptions_.stopBits);
|
||||||
|
}
|
||||||
|
if (!this.isValidParity(this.serialOptions_.parity)) {
|
||||||
|
throw new RangeError('invalid parity ' + this.serialOptions_.parity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Checks the baud rate for validity
|
||||||
|
* @param {number} baudRate the baud rate to check
|
||||||
|
* @return {boolean} A boolean that reflects whether the baud rate is valid
|
||||||
|
*/
|
||||||
|
isValidBaudRate(baudRate) {
|
||||||
|
return baudRate % 1 === 0;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Checks the data bits for validity
|
||||||
|
* @param {number} dataBits the data bits to check
|
||||||
|
* @return {boolean} A boolean that reflects whether the data bits setting is
|
||||||
|
* valid
|
||||||
|
*/
|
||||||
|
isValidDataBits(dataBits) {
|
||||||
|
if (typeof dataBits === 'undefined') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return kAcceptableDataBits.includes(dataBits);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Checks the stop bits for validity
|
||||||
|
* @param {number} stopBits the stop bits to check
|
||||||
|
* @return {boolean} A boolean that reflects whether the stop bits setting is
|
||||||
|
* valid
|
||||||
|
*/
|
||||||
|
isValidStopBits(stopBits) {
|
||||||
|
if (typeof stopBits === 'undefined') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return kAcceptableStopBits.includes(stopBits);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Checks the parity for validity
|
||||||
|
* @param {string} parity the parity to check
|
||||||
|
* @return {boolean} A boolean that reflects whether the parity is valid
|
||||||
|
*/
|
||||||
|
isValidParity(parity) {
|
||||||
|
if (typeof parity === 'undefined') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return kAcceptableParity.includes(parity);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* sends the options alog the control interface to set them on the device
|
||||||
|
* @return {Promise} a promise that will resolve when the options are set
|
||||||
|
*/
|
||||||
|
async setLineCoding() {
|
||||||
|
var _a, _b, _c;
|
||||||
|
// Ref: USB CDC specification version 1.1 §6.2.12.
|
||||||
|
const buffer = new ArrayBuffer(7);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
view.setUint32(0, this.serialOptions_.baudRate, true);
|
||||||
|
view.setUint8(4, kStopBitsIndexMapping.indexOf((_a = this.serialOptions_.stopBits) !== null && _a !== void 0 ? _a : kDefaultStopBits));
|
||||||
|
view.setUint8(5, kParityIndexMapping.indexOf((_b = this.serialOptions_.parity) !== null && _b !== void 0 ? _b : kDefaultParity));
|
||||||
|
view.setUint8(6, (_c = this.serialOptions_.dataBits) !== null && _c !== void 0 ? _c : kDefaultDataBits);
|
||||||
|
const result = await this.device_.controlTransferOut({
|
||||||
|
'requestType': 'class',
|
||||||
|
'recipient': 'interface',
|
||||||
|
'request': kSetLineCoding,
|
||||||
|
'value': 0x00,
|
||||||
|
'index': this.controlInterface_.interfaceNumber,
|
||||||
|
}, buffer);
|
||||||
|
if (result.status != 'ok') {
|
||||||
|
throw new DOMException('NetworkError', 'Failed to set line coding.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** implementation of the global navigator.serial object */
|
||||||
|
class Serial {
|
||||||
|
/**
|
||||||
|
* Requests permission to access a new port.
|
||||||
|
*
|
||||||
|
* @param {SerialPortRequestOptions} options
|
||||||
|
* @param {SerialPolyfillOptions} polyfillOptions
|
||||||
|
* @return {Promise<SerialPort>}
|
||||||
|
*/
|
||||||
|
async requestPort(options, polyfillOptions) {
|
||||||
|
polyfillOptions = Object.assign(Object.assign({}, kDefaultPolyfillOptions), polyfillOptions);
|
||||||
|
const usbFilters = [];
|
||||||
|
if (options && options.filters) {
|
||||||
|
for (const filter of options.filters) {
|
||||||
|
const usbFilter = {
|
||||||
|
classCode: polyfillOptions.usbControlInterfaceClass,
|
||||||
|
};
|
||||||
|
if (filter.usbVendorId !== undefined) {
|
||||||
|
usbFilter.vendorId = filter.usbVendorId;
|
||||||
|
}
|
||||||
|
if (filter.usbProductId !== undefined) {
|
||||||
|
usbFilter.productId = filter.usbProductId;
|
||||||
|
}
|
||||||
|
usbFilters.push(usbFilter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (usbFilters.length === 0) {
|
||||||
|
usbFilters.push({
|
||||||
|
classCode: polyfillOptions.usbControlInterfaceClass,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const device = await navigator.usb.requestDevice({ 'filters': usbFilters });
|
||||||
|
const port = new SerialPort(device, polyfillOptions);
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get the set of currently available ports.
|
||||||
|
*
|
||||||
|
* @param {SerialPolyfillOptions} polyfillOptions Polyfill configuration that
|
||||||
|
* should be applied to these ports.
|
||||||
|
* @return {Promise<SerialPort[]>} a promise that is resolved with a list of
|
||||||
|
* ports.
|
||||||
|
*/
|
||||||
|
async getPorts(polyfillOptions) {
|
||||||
|
polyfillOptions = Object.assign(Object.assign({}, kDefaultPolyfillOptions), polyfillOptions);
|
||||||
|
const devices = await navigator.usb.getDevices();
|
||||||
|
const ports = [];
|
||||||
|
devices.forEach((device) => {
|
||||||
|
try {
|
||||||
|
const port = new SerialPort(device, polyfillOptions);
|
||||||
|
ports.push(port);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// Skip unrecognized port.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ports;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* an object to be used for starting the serial workflow */
|
||||||
|
export const serial = new Serial();
|
||||||
|
//# sourceMappingURL=serial.js.map
|
||||||
1
src/frontend/public/rnode-flasher/js/zip.min.js
vendored
Normal file
1
src/frontend/public/rnode-flasher/js/zip.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
src/frontend/public/rnode-flasher/reticulum_logo_512.png
Normal file
BIN
src/frontend/public/rnode-flasher/reticulum_logo_512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
Reference in New Issue
Block a user