1
0
mirror of https://github.com/chirpstack/chirpstack.git synced 2025-04-17 20:50:14 +00:00

Compare commits

...

9 Commits

Author SHA1 Message Date
927a68a436 Bump version to 4.1.3 2022-12-27 11:14:43 +00:00
417179ba54 Fix header z-index issue in UI.
The z-index of the header was set to 2000 because of Leaflet JS zoom
controls going over the header instead of under when scrolling. However,
this caused issues with the notifications and dropdowns (menu and
autocomplete).

Setting the z-index to 1001 is enough to fix the Leaflet JS issues,
without causing other issues.
2022-12-24 10:46:23 +00:00
1813e6a7b2 Fix redis_key implementation. 2022-12-22 21:04:40 +00:00
e2682db6e2 Bump version to 4.1.2 2022-12-16 09:44:47 +00:00
aa9923a60b Handle integration events async of uplink / downlink flow.
Wrapping the handling of integration events in a tokio::spawn should
already have been there, as we do not want to delay the downlink in case
of slow integrations.
2022-12-15 21:33:23 +00:00
fd061d4657 Bump version to 4.1.1 2022-12-13 13:48:42 +00:00
e3fae6260b Make get device-session for phypayload functions update f_cnt.
This fixes the FrmPayload decryption in case of frame-counter rollover
(16lsb) as it was using the f_cnt as sent over the air (16lsb) and not
the full frame-counter (32b).

Before, these functions would return the device-session for the given
uplink PhyPayload (if a matching device-session was found), together
with the full frame-counter. However it would not modify the f_cnt of
the PhyPayload to the full frame-counter making it prone to errors like
the above.
2022-12-13 12:44:00 +00:00
07d4e89a92 Update JS API dependencies to latest versions. 2022-12-13 10:57:54 +00:00
8e7f321e93 [Rust API] Replace relative paths to .proto files with absolute paths () 2022-12-13 10:37:56 +00:00
26 changed files with 466 additions and 188 deletions

8
Cargo.lock generated

@ -672,7 +672,7 @@ dependencies = [
[[package]]
name = "backend"
version = "4.1.0"
version = "4.1.3"
dependencies = [
"aes-kw",
"anyhow",
@ -889,7 +889,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chirpstack"
version = "4.1.0"
version = "4.1.3"
dependencies = [
"aes",
"anyhow",
@ -968,7 +968,7 @@ dependencies = [
[[package]]
name = "chirpstack_api"
version = "4.1.0"
version = "4.1.3"
dependencies = [
"hex",
"pbjson",
@ -2207,7 +2207,7 @@ dependencies = [
[[package]]
name = "lrwn"
version = "4.1.0"
version = "4.1.3"
dependencies = [
"aes",
"anyhow",

@ -1,6 +1,6 @@
{
"name": "@chirpstack/chirpstack-api-grpc-web",
"version": "4.1.0",
"version": "4.1.3",
"description": "Chirpstack gRPC-web API",
"license": "MIT",
"devDependencies": {

14
api/js/package.json vendored

@ -1,17 +1,17 @@
{
"name": "@chirpstack/chirpstack-api",
"version": "4.1.0",
"version": "4.1.3",
"description": "Chirpstack JS and TS API",
"license": "MIT",
"devDependencies": {
"grpc-tools": "^1.11.2",
"grpc-tools": "^1.12.3",
"ts-protoc-gen": "^0.15.0",
"typescript": "^4.3.5"
"typescript": "^4.9.4"
},
"dependencies": {
"@grpc/grpc-js": "^1.3.7",
"@mapbox/node-pre-gyp": "^1.0.5",
"@types/google-protobuf": "^3.15.5",
"google-protobuf": "^3.17.3"
"@grpc/grpc-js": "^1.8.0",
"@mapbox/node-pre-gyp": "^1.0.10",
"@types/google-protobuf": "^3.15.6",
"google-protobuf": "^3.21.2"
}
}

82
api/js/yarn.lock vendored

@ -2,25 +2,40 @@
# yarn lockfile v1
"@grpc/grpc-js@^1.3.7":
version "1.6.2"
resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.6.2.tgz#fbaceefd163f4886e39501aea32a19c0fe802232"
integrity sha512-9+89Ne1K8F9u86T+l1yIV2DS+dWHYVK61SsDZN4MFTFehOOaJ4rHxa1cW8Lwdn2/6tOx7N3+SY/vfcjztOHopA==
"@grpc/grpc-js@^1.8.0":
version "1.8.0"
resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.8.0.tgz#ebfbeff2b76e2991f2831e46cad27fa573396555"
integrity sha512-ySMTXQuMvvswoobvN+0LsaPf7ITO2JVfJmHxQKI4cGehNrrUms+n81BlHEX7Hl/LExji6XE3fnI9U04GSkRruA==
dependencies:
"@grpc/proto-loader" "^0.6.4"
"@grpc/proto-loader" "^0.7.0"
"@types/node" ">=12.12.47"
"@grpc/proto-loader@^0.6.4":
version "0.6.9"
resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.6.9.tgz#4014eef366da733f8e04a9ddd7376fe8a58547b7"
integrity sha512-UlcCS8VbsU9d3XTXGiEVFonN7hXk+oMXZtoHHG2oSA1/GcDP1q6OUgs20PzHDGizzyi8ufGSUDlk3O2NyY7leg==
"@grpc/proto-loader@^0.7.0":
version "0.7.4"
resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.4.tgz#4946a84fbf47c3ddd4e6a97acb79d69a9f47ebf2"
integrity sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==
dependencies:
"@types/long" "^4.0.1"
lodash.camelcase "^4.3.0"
long "^4.0.0"
protobufjs "^6.10.0"
protobufjs "^7.0.0"
yargs "^16.2.0"
"@mapbox/node-pre-gyp@^1.0.10":
version "1.0.10"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz#8e6735ccebbb1581e5a7e652244cadc8a844d03c"
integrity sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==
dependencies:
detect-libc "^2.0.0"
https-proxy-agent "^5.0.0"
make-dir "^3.1.0"
node-fetch "^2.6.7"
nopt "^5.0.0"
npmlog "^5.0.1"
rimraf "^3.0.2"
semver "^7.3.5"
tar "^6.1.11"
"@mapbox/node-pre-gyp@^1.0.5":
version "1.0.9"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz#09a8781a3a036151cdebbe8719d6f8b25d4058bc"
@ -89,10 +104,10 @@
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
"@types/google-protobuf@^3.15.5":
version "3.15.5"
resolved "https://registry.yarnpkg.com/@types/google-protobuf/-/google-protobuf-3.15.5.tgz#644b2be0f5613b1f822c70c73c6b0e0b5b5fa2ad"
integrity sha512-6bgv24B+A2bo9AfzReeg5StdiijKzwwnRflA8RLd1V4Yv995LeTmo0z69/MPbBDFSiZWdZHQygLo/ccXhMEDgw==
"@types/google-protobuf@^3.15.6":
version "3.15.6"
resolved "https://registry.yarnpkg.com/@types/google-protobuf/-/google-protobuf-3.15.6.tgz#674a69493ef2c849b95eafe69167ea59079eb504"
integrity sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==
"@types/long@^4.0.1":
version "4.0.2"
@ -266,15 +281,20 @@ glob@^7.1.3:
once "^1.3.0"
path-is-absolute "^1.0.0"
google-protobuf@^3.15.5, google-protobuf@^3.17.3:
google-protobuf@^3.15.5:
version "3.20.0"
resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.20.0.tgz#8705ab5fb7e91e9578250a4a8ac533a3cc0bc0bb"
integrity sha512-hhXv5IKLDIkb0pEm53G053UZGhRAhw3wM5Jk7ly5sGIQRkO1s63FaDqM9QjlrPHygKEE2awUlLP9fFrG6M9vfQ==
grpc-tools@^1.11.2:
version "1.11.2"
resolved "https://registry.yarnpkg.com/grpc-tools/-/grpc-tools-1.11.2.tgz#22d802d40012510ccc6591d11f9c94109ac07aab"
integrity sha512-4+EgpnnkJraamY++oyBCw5Hp9huRYfgakjNVKbiE3PgO9Tv5ydVlRo7ZyGJ0C0SEiA7HhbVc1sNNtIyK7FiEtg==
google-protobuf@^3.21.2:
version "3.21.2"
resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.21.2.tgz#4580a2bea8bbb291ee579d1fefb14d6fa3070ea4"
integrity sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==
grpc-tools@^1.12.3:
version "1.12.3"
resolved "https://registry.yarnpkg.com/grpc-tools/-/grpc-tools-1.12.3.tgz#bedbb880e564a52b192d693300280ed7ab45e61d"
integrity sha512-KJgk65dbGqkMuj0xiuT5uk45GcqrFfWTSqpk6Ktd0Ds2cEe9QtPQG/uWCGk185ShXCdgYFLRwfh+FyjQ3nlBNw==
dependencies:
"@mapbox/node-pre-gyp" "^1.0.5"
@ -319,6 +339,11 @@ long@^4.0.0:
resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
long@^5.0.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/long/-/long-5.2.1.tgz#e27595d0083d103d2fa2c20c7699f8e0c92b897f"
integrity sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
@ -406,10 +431,10 @@ path-is-absolute@^1.0.0:
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
protobufjs@^6.10.0:
version "6.11.3"
resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.3.tgz#637a527205a35caa4f3e2a9a4a13ddffe0e7af74"
integrity sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==
protobufjs@^7.0.0:
version "7.1.2"
resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.1.2.tgz#a0cf6aeaf82f5625bffcf5a38b7cd2a7de05890c"
integrity sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==
dependencies:
"@protobufjs/aspromise" "^1.1.2"
"@protobufjs/base64" "^1.1.2"
@ -421,9 +446,8 @@ protobufjs@^6.10.0:
"@protobufjs/path" "^1.1.2"
"@protobufjs/pool" "^1.1.0"
"@protobufjs/utf8" "^1.1.0"
"@types/long" "^4.0.1"
"@types/node" ">=13.7.0"
long "^4.0.0"
long "^5.0.0"
readable-stream@^3.6.0:
version "3.6.0"
@ -520,10 +544,10 @@ ts-protoc-gen@^0.15.0:
dependencies:
google-protobuf "^3.15.5"
typescript@^4.3.5:
version "4.6.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c"
integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==
typescript@^4.9.4:
version "4.9.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78"
integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==
util-deprecate@^1.0.1:
version "1.0.2"

@ -18,7 +18,7 @@ CLASSIFIERS = [
setup(
name='chirpstack-api',
version = "4.1.0",
version = "4.1.3",
url='https://github.com/brocaar/chirpstack-api',
author='Orne Brocaar',
author_email='info@brocaar.com',

2
api/rust/Cargo.lock generated vendored

@ -117,7 +117,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chirpstack_api"
version = "4.1.0"
version = "4.1.3"
dependencies = [
"hex",
"pbjson",

2
api/rust/Cargo.toml vendored

@ -1,7 +1,7 @@
[package]
name = "chirpstack_api"
description = "ChirpStack Protobuf / gRPC API definitions."
version = "4.1.0"
version = "4.1.3"
authors = ["Orne Brocaar <info@brocaar.com>"]
license = "MIT"
homepage = "https://www.chirpstack.io"

62
api/rust/build.rs vendored

@ -7,6 +7,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let proto_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let proto_dir = Path::new(&proto_dir);
let proto_dir = proto_dir.join("proto");
let cs_dir = proto_dir.join("chirpstack");
std::fs::create_dir_all(out_dir.join("common")).unwrap();
std::fs::create_dir_all(out_dir.join("gw")).unwrap();
@ -22,7 +23,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.compile_well_known_types(true)
.extern_path(".google.protobuf", "::pbjson_types")
.compile(
&["common/common.proto"],
&[cs_dir.join("common").join("common.proto").to_str().unwrap()],
&[
proto_dir.join("chirpstack").to_str().unwrap(),
proto_dir.join("google").to_str().unwrap(),
@ -43,7 +44,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.extern_path(".google.protobuf", "::pbjson_types")
.extern_path(".common", "crate::common")
.compile(
&["gw/gw.proto"],
&[cs_dir.join("gw").join("gw.proto").to_str().unwrap()],
&[
proto_dir.join("chirpstack").to_str().unwrap(),
proto_dir.join("google").to_str().unwrap(),
@ -65,7 +66,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.extern_path(".google.protobuf", "::pbjson_types")
.extern_path(".common", "crate::common")
.compile(
&["internal/internal.proto"],
&[cs_dir
.join("internal")
.join("internal.proto")
.to_str()
.unwrap()],
&[
proto_dir.join("chirpstack").to_str().unwrap(),
proto_dir.join("google").to_str().unwrap(),
@ -88,7 +93,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.extern_path(".common", "crate::common")
.extern_path(".gw", "crate::gw")
.compile(
&["integration/integration.proto"],
&[cs_dir
.join("integration")
.join("integration.proto")
.to_str()
.unwrap()],
&[
proto_dir.join("chirpstack").to_str().unwrap(),
proto_dir.join("google").to_str().unwrap(),
@ -112,7 +121,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.extern_path(".common", "crate::common")
.extern_path(".gw", "crate::gw")
.compile(
&["meta/meta.proto"],
&[cs_dir.join("meta").join("meta.proto").to_str().unwrap()],
&[proto_dir.join("chirpstack").to_str().unwrap()],
)?;
@ -132,18 +141,37 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.extern_path(".gw", "crate::gw")
.compile(
&[
"api/internal.proto",
"api/user.proto",
"api/tenant.proto",
"api/application.proto",
"api/device_profile.proto",
"api/device_profile_template.proto",
"api/device.proto",
"api/gateway.proto",
"api/frame_log.proto",
"api/multicast_group.proto",
"api/frame_log.proto",
"api/request_log.proto",
cs_dir.join("api").join("internal.proto").to_str().unwrap(),
cs_dir.join("api").join("user.proto").to_str().unwrap(),
cs_dir.join("api").join("tenant.proto").to_str().unwrap(),
cs_dir
.join("api")
.join("application.proto")
.to_str()
.unwrap(),
cs_dir
.join("api")
.join("device_profile.proto")
.to_str()
.unwrap(),
cs_dir
.join("api")
.join("device_profile_template.proto")
.to_str()
.unwrap(),
cs_dir.join("api").join("device.proto").to_str().unwrap(),
cs_dir.join("api").join("gateway.proto").to_str().unwrap(),
cs_dir.join("api").join("frame_log.proto").to_str().unwrap(),
cs_dir
.join("api")
.join("multicast_group.proto")
.to_str()
.unwrap(),
cs_dir
.join("api")
.join("request_log.proto")
.to_str()
.unwrap(),
],
&[
proto_dir.join("chirpstack").to_str().unwrap(),

@ -1,6 +1,6 @@
[package]
name = "backend"
version = "4.1.0"
version = "4.1.3"
authors = ["Orne Brocaar <info@brocaar.com>"]
edition = "2018"
publish = false

@ -3,7 +3,7 @@ name = "chirpstack"
description = "ChirpStack is an open-source LoRaWAN(TM) Network Server"
repository = "https://github.com/chirpstack/chirpstack"
homepage="https://www.chirpstack.io/"
version = "4.1.0"
version = "4.1.3"
authors = ["Orne Brocaar <info@brocaar.com>"]
edition = "2021"
publish = false

@ -262,7 +262,7 @@ async fn _handle_pr_start_req_data(
let region_name = region::get_region_name(region_common_name)?;
let dr = pl.ul_meta_data.data_rate.unwrap_or_default();
let ufs = UplinkFrameSet {
let mut ufs = UplinkFrameSet {
uplink_set_id: Uuid::new_v4(),
dr,
ch: helpers::get_uplink_ch(&region_name, tx_info.frequency, dr)?,
@ -280,7 +280,7 @@ async fn _handle_pr_start_req_data(
};
// get device-session
let ds = device_session::get_for_phypayload(&ufs.phy_payload, ufs.dr, ufs.ch as u8).await?;
let ds = device_session::get_for_phypayload(&mut ufs.phy_payload, ufs.dr, ufs.ch as u8).await?;
let pr_lifetime = roaming::get_passive_roaming_lifetime(sender_id)?;
let kek_label = roaming::get_passive_roaming_kek_label(sender_id)?;

@ -333,10 +333,7 @@ impl Data {
},
};
integration::ack_event(&self.application.id, &self.device.variables, &pl)
.await
.context("Publish ack event")?;
integration::ack_event(self.application.id, &self.device.variables, &pl).await;
warn!(dev_eui = %self.device.dev_eui, device_queue_item_id = %qi.id, "Device queue-item discarded because of timeout");
continue;
@ -366,10 +363,7 @@ impl Data {
.collect(),
};
integration::log_event(&self.application.id, &self.device.variables, &pl)
.await
.context("Publish log event")?;
integration::log_event(self.application.id, &self.device.variables, &pl).await;
warn!(dev_eui = %self.device.dev_eui, device_queue_item_id = %qi.id, "Device queue-item discarded because of max. payload size");
continue;

@ -318,7 +318,7 @@ impl TxAck {
..Default::default()
};
integration::log_event(&app.id, &dev.variables, &pl).await?;
integration::log_event(app.id, &dev.variables, &pl).await;
Ok(())
}
@ -366,7 +366,7 @@ impl TxAck {
tx_info: self.downlink_frame_item.as_ref().unwrap().tx_info.clone(),
};
integration::txack_event(&app.id, &dev.variables, &pl).await?;
integration::txack_event(app.id, &dev.variables, &pl).await;
Ok(())
}

@ -378,7 +378,8 @@ impl Integration {
)?)),
};
integration_event(&Uuid::from_str(&di.application_id)?, vars, &int_pl).await
integration_event(Uuid::from_str(&di.application_id)?, vars, &int_pl).await;
Ok(())
}
async fn handle_response_downlink(
@ -431,7 +432,8 @@ impl Integration {
}),
};
location_event(&Uuid::from_str(&di.application_id)?, vars, &loc_pl).await
location_event(Uuid::from_str(&di.application_id)?, vars, &loc_pl).await;
Ok(())
}
async fn update_geoloc_buffer(
@ -729,7 +731,7 @@ impl IntegrationTrait for Integration {
location: Some(v),
};
location_event(&Uuid::from_str(&di.application_id)?, vars, &loc_pl).await?;
location_event(Uuid::from_str(&di.application_id)?, vars, &loc_pl).await;
}
Ok(())

@ -133,7 +133,7 @@ pub trait Integration {
}
// Returns a Vec of integrations for the given Application ID.
async fn for_application_id(id: &Uuid) -> Result<Vec<Box<dyn Integration + Sync + Send>>> {
async fn for_application_id(id: Uuid) -> Result<Vec<Box<dyn Integration + Sync + Send>>> {
#[cfg(test)]
{
let m = MOCK_INTEGRATION.read().await;
@ -143,7 +143,7 @@ async fn for_application_id(id: &Uuid) -> Result<Vec<Box<dyn Integration + Sync
}
let mut out: Vec<Box<dyn Integration + Sync + Send>> = Vec::new();
let integrations = application::get_integrations_for_application(id).await?;
let integrations = application::get_integrations_for_application(&id).await?;
for app_i in &integrations {
out.push(match &app_i.configuration {
@ -187,7 +187,24 @@ async fn for_application_id(id: &Uuid) -> Result<Vec<Box<dyn Integration + Sync
}
pub async fn uplink_event(
application_id: &Uuid,
application_id: Uuid,
vars: &HashMap<String, String>,
pl: &integration::UplinkEvent,
) {
tokio::spawn({
let vars = vars.clone();
let pl = pl.clone();
async move {
if let Err(err) = _uplink_event(application_id, &vars, &pl).await {
error!(application_id = %application_id, error = %err, "Uplink event error");
}
}
});
}
async fn _uplink_event(
application_id: Uuid,
vars: &HashMap<String, String>,
pl: &integration::UplinkEvent,
) -> Result<()> {
@ -212,7 +229,24 @@ pub async fn uplink_event(
}
pub async fn join_event(
application_id: &Uuid,
application_id: Uuid,
vars: &HashMap<String, String>,
pl: &integration::JoinEvent,
) {
tokio::spawn({
let vars = vars.clone();
let pl = pl.clone();
async move {
if let Err(err) = _join_event(application_id, &vars, &pl).await {
error!(application_id = %application_id, error = %err, "Join event error");
}
}
});
}
async fn _join_event(
application_id: Uuid,
vars: &HashMap<String, String>,
pl: &integration::JoinEvent,
) -> Result<()> {
@ -237,7 +271,24 @@ pub async fn join_event(
}
pub async fn ack_event(
application_id: &Uuid,
application_id: Uuid,
vars: &HashMap<String, String>,
pl: &integration::AckEvent,
) {
tokio::spawn({
let vars = vars.clone();
let pl = pl.clone();
async move {
if let Err(err) = _ack_event(application_id, &vars, &pl).await {
error!(application_id = %application_id, error = %err, "Ack event error");
}
}
});
}
async fn _ack_event(
application_id: Uuid,
vars: &HashMap<String, String>,
pl: &integration::AckEvent,
) -> Result<()> {
@ -262,7 +313,24 @@ pub async fn ack_event(
}
pub async fn txack_event(
application_id: &Uuid,
application_id: Uuid,
vars: &HashMap<String, String>,
pl: &integration::TxAckEvent,
) {
tokio::spawn({
let vars = vars.clone();
let pl = pl.clone();
async move {
if let Err(err) = _txack_event(application_id, &vars, &pl).await {
error!(application_id = %application_id, error = %err, "Txack event error");
}
}
});
}
async fn _txack_event(
application_id: Uuid,
vars: &HashMap<String, String>,
pl: &integration::TxAckEvent,
) -> Result<()> {
@ -287,7 +355,24 @@ pub async fn txack_event(
}
pub async fn log_event(
application_id: &Uuid,
application_id: Uuid,
vars: &HashMap<String, String>,
pl: &integration::LogEvent,
) {
tokio::spawn({
let vars = vars.clone();
let pl = pl.clone();
async move {
if let Err(err) = _log_event(application_id, &vars, &pl).await {
error!(application_id = %application_id, error = %err, "Log event error");
}
}
});
}
async fn _log_event(
application_id: Uuid,
vars: &HashMap<String, String>,
pl: &integration::LogEvent,
) -> Result<()> {
@ -312,7 +397,24 @@ pub async fn log_event(
}
pub async fn status_event(
application_id: &Uuid,
application_id: Uuid,
vars: &HashMap<String, String>,
pl: &integration::StatusEvent,
) {
tokio::spawn({
let vars = vars.clone();
let pl = pl.clone();
async move {
if let Err(err) = _status_event(application_id, &vars, &pl).await {
error!(application_id = %application_id, error = %err, "Status event error");
}
}
});
}
async fn _status_event(
application_id: Uuid,
vars: &HashMap<String, String>,
pl: &integration::StatusEvent,
) -> Result<()> {
@ -337,7 +439,24 @@ pub async fn status_event(
}
pub async fn location_event(
application_id: &Uuid,
application_id: Uuid,
vars: &HashMap<String, String>,
pl: &integration::LocationEvent,
) {
tokio::spawn({
let vars = vars.clone();
let pl = pl.clone();
async move {
if let Err(err) = _location_event(application_id, &vars, &pl).await {
error!(application_id = %application_id, error = %err, "Location event error");
}
}
});
}
async fn _location_event(
application_id: Uuid,
vars: &HashMap<String, String>,
pl: &integration::LocationEvent,
) -> Result<()> {
@ -362,7 +481,24 @@ pub async fn location_event(
}
pub async fn integration_event(
application_id: &Uuid,
application_id: Uuid,
vars: &HashMap<String, String>,
pl: &integration::IntegrationEvent,
) {
tokio::spawn({
let vars = vars.clone();
let pl = pl.clone();
async move {
if let Err(err) = _integration_event(application_id, &vars, &pl).await {
error!(application_id = %application_id, error = %err, "Location event error");
}
}
});
}
async fn _integration_event(
application_id: Uuid,
vars: &HashMap<String, String>,
pl: &integration::IntegrationEvent,
) -> Result<()> {

@ -1,7 +1,7 @@
use anyhow::Result;
use bigdecimal::BigDecimal;
use chrono::{DateTime, Utc};
use tracing::{error, info};
use tracing::info;
use crate::integration;
use crate::storage::{application, device, device_profile, tenant};
@ -48,8 +48,8 @@ pub async fn handle(
let rx_time: DateTime<Utc> =
helpers::get_rx_timestamp(&uplink_frame_set.rx_info_set).into();
if let Err(e) = integration::status_event(
&app.id,
integration::status_event(
app.id,
&dev.variables,
&integration_pb::StatusEvent {
deduplication_id: uplink_frame_set.uplink_set_id.to_string(),
@ -75,10 +75,7 @@ pub async fn handle(
},
},
)
.await
{
error!(error = %e, "Sending status event error");
}
.await;
}
Ok(None)
@ -94,6 +91,8 @@ pub mod test {
use lrwn::EUI64;
use std::collections::HashMap;
use std::str::FromStr;
use std::time::Duration;
use tokio::time::sleep;
use uuid::Uuid;
#[test]
@ -189,6 +188,9 @@ pub mod test {
.unwrap();
assert_eq!(true, resp.is_none());
// Integration events are handled async.
sleep(Duration::from_millis(100)).await;
let status_events = mock::get_status_events().await;
assert_eq!(
vec![integration_pb::StatusEvent {

@ -122,15 +122,13 @@ pub async fn delete(dev_eui: &EUI64) -> Result<()> {
// This function will increment the uplink frame-counter and will immediately update the
// device-session in the database, to make sure that in case this function is called multiple
// times, at most one will be valid.
// On Ok response, the PhyPayload f_cnt will be set to the full 32bit frame-counter based on the
// device-session context.
pub async fn get_for_phypayload_and_incr_f_cnt_up(
phy: &PhyPayload,
phy: &mut PhyPayload,
tx_dr: u8,
tx_ch: u8,
) -> Result<ValidationStatus, Error> {
// Clone the PhyPayload, as we will update the f_cnt to the full (32bit) frame-counter value
// for calculating the MIC.
let mut phy = phy.clone();
let mut _dev_addr = DevAddr::from_be_bytes([0x00, 0x00, 0x00, 0x00]);
let mut _f_cnt_orig = 0;
@ -150,11 +148,6 @@ pub async fn get_for_phypayload_and_incr_f_cnt_up(
}
for mut ds in device_sessions {
// Restore the original f_cnt.
if let Payload::MACPayload(pl) = &mut phy.payload {
pl.fhdr.f_cnt = _f_cnt_orig;
}
// Get the full 32bit frame-counter.
let full_f_cnt = get_full_f_cnt_up(ds.f_cnt_up, _f_cnt_orig);
let f_nwk_s_int_key = AES128Key::from_slice(&ds.f_nwk_s_int_key)?;
@ -236,6 +229,11 @@ pub async fn get_for_phypayload_and_incr_f_cnt_up(
return Ok(ValidationStatus::Reset(full_f_cnt, ds));
}
}
// Restore the original f_cnt.
if let Payload::MACPayload(pl) = &mut phy.payload {
pl.fhdr.f_cnt = _f_cnt_orig;
}
}
Err(Error::InvalidMIC)
@ -244,15 +242,13 @@ pub async fn get_for_phypayload_and_incr_f_cnt_up(
// Simmilar to get_for_phypayload_and_incr_f_cnt_up, but only retrieves the device-session for the
// given PhyPayload. As it does not return the ValidationStatus, it only returns the DeviceSession
// in case of a valid frame-counter.
// On Ok response, the PhyPayload f_cnt will be set to the full 32bit frame-counter based on the
// device-session context.
pub async fn get_for_phypayload(
phy: &PhyPayload,
phy: &mut PhyPayload,
tx_dr: u8,
tx_ch: u8,
) -> Result<internal::DeviceSession, Error> {
// Clone the PhyPayload, as we will update the f_cnt to the full (32bit) frame-counter value
// for calculating the MIC.
let mut phy = phy.clone();
// Get the dev_addr and original f_cnt.
let (dev_addr, f_cnt_orig) = if let Payload::MACPayload(pl) = &phy.payload {
(pl.fhdr.devaddr, pl.fhdr.f_cnt)
@ -268,11 +264,6 @@ pub async fn get_for_phypayload(
}
for ds in device_sessions {
// Restore the original f_cnt.
if let Payload::MACPayload(pl) = &mut phy.payload {
pl.fhdr.f_cnt = f_cnt_orig;
}
// Get the full 32bit frame-counter.
let full_f_cnt = get_full_f_cnt_up(ds.f_cnt_up, f_cnt_orig);
let f_nwk_s_int_key = AES128Key::from_slice(&ds.f_nwk_s_int_key)?;
@ -297,6 +288,11 @@ pub async fn get_for_phypayload(
if mic_ok && full_f_cnt >= ds.f_cnt_up {
return Ok(ds);
}
// Restore the original f_cnt.
if let Payload::MACPayload(pl) = &mut phy.payload {
pl.fhdr.f_cnt = f_cnt_orig;
}
}
Err(Error::InvalidMIC)
@ -511,6 +507,24 @@ pub mod test {
})),
..Default::default()
},
internal::DeviceSession {
dev_addr: vec![0x01, 0x02, 0x03, 0x04],
dev_eui: vec![0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05],
s_nwk_s_int_key: vec![
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
0x05, 0x05, 0x05,
],
f_nwk_s_int_key: vec![
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
0x05, 0x05, 0x05,
],
nwk_s_enc_key: vec![
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
0x05, 0x05, 0x05,
],
f_cnt_up: (1 << 16) + 1,
..Default::default()
},
];
for ds in &device_sessions {
@ -628,6 +642,24 @@ pub mod test {
expected_error: None,
expected_reset: false,
},
Test {
name: "frame-counter rollover (16lsb)".to_string(),
dev_addr: DevAddr::from_be_bytes([0x01, 0x02, 0x03, 0x04]),
f_nwk_s_int_key: AES128Key::from_bytes([
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
0x05, 0x05, 0x05,
]),
s_nwk_s_int_key: AES128Key::from_bytes([
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
0x05, 0x05, 0x05,
]),
f_cnt: (1 << 16) + 11,
expected_dev_eui: EUI64::from_slice(&device_sessions[3].dev_eui).unwrap(),
expected_fcnt_up: (1 << 16) + 11,
expected_retransmission: false,
expected_error: None,
expected_reset: false,
},
];
for tst in &tests {
@ -658,15 +690,29 @@ pub mod test {
)
.unwrap();
let ds_res = get_for_phypayload_and_incr_f_cnt_up(&phy, 0, 0).await;
// Truncate to 16LSB (as it would be transmitted over the air).
if let lrwn::Payload::MACPayload(pl) = &mut phy.payload {
pl.fhdr.f_cnt = tst.f_cnt % (1 << 16);
}
let ds_res = get_for_phypayload_and_incr_f_cnt_up(&mut phy, 0, 0).await;
if tst.expected_error.is_some() {
assert_eq!(true, ds_res.is_err());
assert_eq!(
tst.expected_error.as_ref().unwrap(),
&ds_res.err().unwrap().to_string()
);
if let lrwn::Payload::MACPayload(pl) = &phy.payload {
assert_eq!(tst.f_cnt, pl.fhdr.f_cnt);
}
} else {
let ds = ds_res.unwrap();
// Validate that the f_cnt of the PhyPayload was set to the full frame-counter.
if let lrwn::Payload::MACPayload(pl) = &phy.payload {
assert_eq!(tst.expected_fcnt_up, pl.fhdr.f_cnt);
}
if let ValidationStatus::Ok(full_f_cnt, ds) = ds {
assert_eq!(false, tst.expected_retransmission);
assert_eq!(

@ -38,6 +38,7 @@ pub type PgPoolConnection = PooledConnection<ConnectionManager<PgConnection>>;
lazy_static! {
static ref PG_POOL: RwLock<Option<PgPool>> = RwLock::new(None);
static ref REDIS_POOL: RwLock<Option<RedisPool>> = RwLock::new(None);
static ref REDIS_PREFIX: RwLock<String> = RwLock::new("".to_string());
}
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations");
@ -210,6 +211,11 @@ pub async fn setup() -> Result<()> {
set_redis_pool(RedisPool::Client(pool));
}
if !conf.redis.key_prefix.is_empty() {
info!(prefix = %conf.redis.key_prefix, "Setting Redis prefix");
*REDIS_PREFIX.write().unwrap() = conf.redis.key_prefix.clone();
}
Ok(())
}
@ -244,7 +250,8 @@ pub fn set_redis_pool(p: RedisPool) {
}
pub fn redis_key(s: String) -> String {
s
let prefix = REDIS_PREFIX.read().unwrap();
format!("{}{}", prefix, s)
}
#[cfg(test)]
@ -264,3 +271,23 @@ pub async fn reset_redis() -> Result<()> {
redis::cmd("FLUSHALL").query(&mut *c)?;
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_prefix_no_prefix() {
*REDIS_PREFIX.write().unwrap() = "".to_string();
assert_eq!("lora:test:key", redis_key("lora:test:key".to_string()));
}
#[test]
fn test_prefix() {
*REDIS_PREFIX.write().unwrap() = "foobar:".to_string();
assert_eq!(
"foobar:lora:test:key",
redis_key("lora:test:key".to_string())
);
}
}

@ -1,10 +1,12 @@
use std::future::Future;
use std::io::Cursor;
use std::pin::Pin;
use std::time::Duration;
use prost::Message;
use redis::streams::StreamReadReply;
use tokio::sync::RwLock;
use tokio::time::sleep;
use crate::gateway::backend::mock as gateway_mock;
use crate::integration::mock;
@ -122,6 +124,9 @@ pub fn integration_log(logs: Vec<String>) -> Validator {
Box::new(move || {
let logs = logs.clone();
Box::pin(async move {
// Integration events are handled async.
sleep(Duration::from_millis(100)).await;
let mock_logs = mock::get_log_events().await;
assert_eq!(logs.len(), mock_logs.len());
@ -136,6 +141,9 @@ pub fn integration_log(logs: Vec<String>) -> Validator {
pub fn no_uplink_event() -> Validator {
Box::new(move || {
Box::pin(async move {
// Integration events are handled async.
sleep(Duration::from_millis(100)).await;
let mock_events = mock::get_uplink_events().await;
assert_eq!(0, mock_events.len());
})
@ -146,6 +154,9 @@ pub fn uplink_event(up: integration_pb::UplinkEvent) -> Validator {
Box::new(move || {
let up = up.clone();
Box::pin(async move {
// Integration events are handled async.
sleep(Duration::from_millis(100)).await;
let mut mock_events = mock::get_uplink_events().await;
assert_eq!(1, mock_events.len());
@ -163,6 +174,9 @@ pub fn ack_event(ack: integration_pb::AckEvent) -> Validator {
Box::new(move || {
let ack = ack.clone();
Box::pin(async move {
// Integration events are handled async.
sleep(Duration::from_millis(100)).await;
let mut mock_events = mock::get_ack_events().await;
assert_eq!(1, mock_events.len());
@ -180,6 +194,9 @@ pub fn status_event(st: integration_pb::StatusEvent) -> Validator {
Box::new(move || {
let st = st.clone();
Box::pin(async move {
// Integration events are handled async.
sleep(Duration::from_millis(100)).await;
let mut mock_events = mock::get_status_events().await;
assert_eq!(1, mock_events.len());

@ -132,53 +132,58 @@ impl Data {
async fn get_device_session(&mut self) -> Result<(), Error> {
trace!("Getting device-session for dev_addr");
if let lrwn::Payload::MACPayload(pl) = &self.uplink_frame_set.phy_payload.payload {
match device_session::get_for_phypayload_and_incr_f_cnt_up(
&self.uplink_frame_set.phy_payload,
self.uplink_frame_set.dr,
self.uplink_frame_set.ch as u8,
)
.await
{
Ok(v) => match v {
device_session::ValidationStatus::Ok(f_cnt, ds) => {
self.device_session = Some(ds);
self.f_cnt_up_full = f_cnt;
}
device_session::ValidationStatus::Retransmission(f_cnt, ds) => {
self.retransmission = true;
self.device_session = Some(ds);
self.f_cnt_up_full = f_cnt;
}
device_session::ValidationStatus::Reset(f_cnt, ds) => {
self.reset = true;
self.device_session = Some(ds);
self.f_cnt_up_full = f_cnt;
}
},
Err(e) => match e {
StorageError::NotFound(s) => {
warn!(dev_addr = %s, "No device-session exists for dev_addr");
return Err(Error::Abort);
}
StorageError::InvalidMIC => {
warn!(dev_addr = %pl.fhdr.devaddr, "None of the device-sessions for dev_addr resulted in valid MIC");
// Log uplink for null DevEUI.
let mut ufl: api::UplinkFrameLog = (&self.uplink_frame_set).try_into()?;
ufl.dev_eui = "0000000000000000".to_string();
framelog::log_uplink_for_device(&ufl).await?;
return Err(Error::Abort);
}
_ => {
return Err(Error::AnyhowError(
anyhow::Error::new(e).context("Get device-session"),
));
}
},
let dev_addr =
if let lrwn::Payload::MACPayload(pl) = &self.uplink_frame_set.phy_payload.payload {
pl.fhdr.devaddr
} else {
return Err(Error::AnyhowError(anyhow!("No MacPayload in PhyPayload")));
};
}
match device_session::get_for_phypayload_and_incr_f_cnt_up(
&mut self.uplink_frame_set.phy_payload,
self.uplink_frame_set.dr,
self.uplink_frame_set.ch as u8,
)
.await
{
Ok(v) => match v {
device_session::ValidationStatus::Ok(f_cnt, ds) => {
self.device_session = Some(ds);
self.f_cnt_up_full = f_cnt;
}
device_session::ValidationStatus::Retransmission(f_cnt, ds) => {
self.retransmission = true;
self.device_session = Some(ds);
self.f_cnt_up_full = f_cnt;
}
device_session::ValidationStatus::Reset(f_cnt, ds) => {
self.reset = true;
self.device_session = Some(ds);
self.f_cnt_up_full = f_cnt;
}
},
Err(e) => match e {
StorageError::NotFound(s) => {
warn!(dev_addr = %s, "No device-session exists for dev_addr");
return Err(Error::Abort);
}
StorageError::InvalidMIC => {
warn!(dev_addr = %dev_addr, "None of the device-sessions for dev_addr resulted in valid MIC");
// Log uplink for null DevEUI.
let mut ufl: api::UplinkFrameLog = (&self.uplink_frame_set).try_into()?;
ufl.dev_eui = "0000000000000000".to_string();
framelog::log_uplink_for_device(&ufl).await?;
return Err(Error::Abort);
}
_ => {
return Err(Error::AnyhowError(
anyhow::Error::new(e).context("Get device-session"),
));
}
},
};
Ok(())
}
@ -310,7 +315,7 @@ impl Data {
.cloned()
.collect(),
};
integration::log_event(&app.id, &dev.variables, &pl).await?;
integration::log_event(app.id, &dev.variables, &pl).await;
}
if self.reset {
@ -328,7 +333,7 @@ impl Data {
.cloned()
.collect(),
};
integration::log_event(&app.id, &dev.variables, &pl).await?;
integration::log_event(app.id, &dev.variables, &pl).await;
}
Err(Error::Abort)
@ -695,7 +700,7 @@ impl Data {
Ok(v) => v,
Err(e) => {
integration::log_event(
&app.id,
app.id,
&dev.variables,
&integration_pb::LogEvent {
time: Some(Utc::now().into()),
@ -709,12 +714,12 @@ impl Data {
.collect(),
},
)
.await?;
.await;
None
}
};
integration::uplink_event(&app.id, &dev.variables, &pl).await?;
integration::uplink_event(app.id, &dev.variables, &pl).await;
self.uplink_event = Some(pl);
@ -866,7 +871,7 @@ impl Data {
tags.extend((*dev.tags).clone());
integration::ack_event(
&app.id,
app.id,
&dev.variables,
&integration_pb::AckEvent {
deduplication_id: self.uplink_frame_set.uplink_set_id.to_string(),
@ -887,7 +892,7 @@ impl Data {
f_cnt_down: qi.f_cnt_down.unwrap_or(0) as u32,
},
)
.await?;
.await;
Ok(())
}

@ -259,7 +259,7 @@ impl JoinRequest {
Err(v) => match v {
StorageError::InvalidDevNonce => {
integration::log_event(
&app.id,
app.id,
&dev.variables,
&integration_pb::LogEvent {
time: Some(Utc::now().into()),
@ -276,7 +276,7 @@ impl JoinRequest {
.collect(),
},
)
.await?;
.await;
metrics::save(
&format!("device:{}", dev.dev_eui),
@ -314,7 +314,7 @@ impl JoinRequest {
let dev = self.device.as_ref().unwrap();
integration::log_event(
&app.id,
app.id,
&dev.variables,
&integration_pb::LogEvent {
time: Some(Utc::now().into()),
@ -331,7 +331,7 @@ impl JoinRequest {
.collect(),
},
)
.await?;
.await;
metrics::save(
&format!("device:{}", dev.dev_eui),
@ -739,7 +739,7 @@ impl JoinRequest {
dev_addr: self.dev_addr.as_ref().unwrap().to_string(),
};
integration::join_event(&app.id, &dev.variables, &pl).await?;
integration::join_event(app.id, &dev.variables, &pl).await;
Ok(())
}
}

@ -301,7 +301,7 @@ impl JoinRequest {
Err(v) => match v {
StorageError::InvalidDevNonce => {
integration::log_event(
&app.id,
app.id,
&dev.variables,
&integration_pb::LogEvent {
time: Some(Utc::now().into()),
@ -318,7 +318,7 @@ impl JoinRequest {
.collect(),
},
)
.await?;
.await;
metrics::save(
&format!("device:{}", dev.dev_eui),
@ -356,7 +356,7 @@ impl JoinRequest {
let dev = self.device.as_ref().unwrap();
integration::log_event(
&app.id,
app.id,
&dev.variables,
&integration_pb::LogEvent {
time: Some(Utc::now().into()),
@ -373,7 +373,7 @@ impl JoinRequest {
.collect(),
},
)
.await?;
.await;
metrics::save(
&format!("device:{}", dev.dev_eui),
@ -676,7 +676,7 @@ impl JoinRequest {
dev_addr: self.dev_addr.as_ref().unwrap().to_string(),
};
integration::join_event(&app.id, &dev.variables, &pl).await?;
integration::join_event(app.id, &dev.variables, &pl).await;
Ok(())
}

@ -3,7 +3,7 @@ name = "lrwn"
description = "Library for encoding / decoding LoRaWAN frames."
homepage = "https://www.chirpstack.io"
license = "MIT"
version = "4.1.0"
version = "4.1.3"
authors = ["Orne Brocaar <info@brocaar.com>"]
edition = "2018"
repository = "https://github.com/chirpstack/chirpstack"

@ -1,6 +1,6 @@
{
"name": "chirpstack-ui",
"version": "4.1.0",
"version": "4.1.3",
"private": true,
"dependencies": {
"@ant-design/colors": "^6.0.0",

@ -1,7 +1,3 @@
.ant-notification {
z-index: 3000;
}
.layout {
margin-left: 300px;
}
@ -9,7 +5,8 @@
.layout-header {
background: #ffffff;
box-shadow: 0px 0px 10px 0px #ccc;
z-index: 2000;
/* Leaflet JS zoom controls have z-index 1000. */
z-index: 1001;
position: fixed;
width: 100%;

@ -1881,7 +1881,7 @@
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@chirpstack/chirpstack-api-grpc-web@file:../api/grpc-web":
version "4.1.0"
version "4.1.3"
dependencies:
"@types/google-protobuf" "^3.15.2"
google-protobuf "^3.17.3"