Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
changed encode/decode processing
- encode/decode now has their own Handler trait functions, no longer relies on js module functions to run
- rquickjs: now uses call function rather than eval_with_option, to allow id/name functions with exports to be used in js plugin file
- encodeDownlink/decodeUplink now needs the export keyword
  • Loading branch information
nicolas-juteau committed Mar 25, 2025
commit bc231d968997d668fde1db27e2518ea461c48719
6 changes: 3 additions & 3 deletions chirpstack/src/codec/js/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ use rquickjs::{CatchResultExt, IntoJs};
use super::convert;
use crate::config;

mod vendor_base64_js;
mod vendor_buffer;
mod vendor_ieee754;
pub mod vendor_base64_js;
pub mod vendor_buffer;
pub mod vendor_ieee754;

pub async fn decode(
recv_time: DateTime<Utc>,
Expand Down
31 changes: 25 additions & 6 deletions chirpstack/src/codec/js_plugin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use async_trait::async_trait;
use std::collections::HashMap;
use tokio::sync::RwLock;
use tracing::{info, trace, warn};
use chrono::{DateTime, Utc};
use crate::config;

pub mod plugin;
Expand Down Expand Up @@ -45,15 +46,28 @@ pub async fn get_plugins() -> HashMap<String, String> {
out
}

pub async fn get_plugin_script(plugin_id: &str) -> String {
pub async fn encode(plugin_id: &str, f_port: u8, variables: &HashMap<String, String>, obj: &prost_types::Struct) -> Result<Vec<u8>> {
let plugins = CODEC_PLUGINS.read().await;
match plugins.get(plugin_id) {
Some(v) => {
v.get_script()
v.encode(f_port, variables, obj).await
},
None => {
warn!(plugin_id = %plugin_id, "No codec plugin configured with given ID");
String::from("")
Err(anyhow!("No codec plugin configured with given ID: {}", plugin_id))
}
}
}

pub async fn decode(plugin_id: &str, recv_time: DateTime<Utc>, f_port: u8, variables: &HashMap<String, String>, b: &[u8]) -> Result<pbjson_types::Struct> {
let plugins = CODEC_PLUGINS.read().await;
match plugins.get(plugin_id) {
Some(v) => {
v.decode(recv_time, f_port, variables, b).await
},
None => {
warn!(plugin_id = %plugin_id, "No codec plugin configured with given ID");
Err(anyhow!("No codec plugin configured with given ID: {}", plugin_id))
}
}
}
Expand All @@ -66,6 +80,11 @@ pub trait Handler {
// Get the ID.
fn get_id(&self) -> String;

// Get the script.
fn get_script(&self) -> String;
}
// Encode downlink
async fn encode(&self, f_port: u8, variables: &HashMap<String, String>, obj: &prost_types::Struct) -> Result<Vec<u8>>;

// Decode uplink
async fn decode(&self, recv_time: DateTime<Utc>, f_port: u8, variables: &HashMap<String, String>, b: &[u8]) -> Result<pbjson_types::Struct>;
}

// TODO: add tests
4 changes: 2 additions & 2 deletions chirpstack/src/codec/js_plugin/passthrough.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub const SCRIPT: &str = r#"
*
* @returns {{data: object}} Object representing the decoded payload.
*/
function decodeUplink(input) {
export function decodeUplink(input) {
return {
data: {
// Empty object
Expand All @@ -29,7 +29,7 @@ function decodeUplink(input) {
*
* @returns {{bytes: number[]}} Byte array containing the downlink payload.
*/
function encodeDownlink(input) {
export function encodeDownlink(input) {
return {
bytes: input.data, // Passthrough
};
Expand Down
164 changes: 161 additions & 3 deletions chirpstack/src/codec/js_plugin/plugin.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
use std::fs;
use std::collections::HashMap;
use std::time::SystemTime;

use anyhow::{Context, Result};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use super::{Handler, passthrough};

use rquickjs::{CatchResultExt, IntoJs};

use super::super::convert;
use super::super::js::vendor_base64_js;
use super::super::js::vendor_buffer;
use super::super::js::vendor_ieee754;

use crate::config;

pub struct Plugin {
script: String,
id: String,
Expand Down Expand Up @@ -56,7 +68,153 @@ impl Handler for Plugin {
self.id.clone()
}

fn get_script(&self) -> String {
self.script.clone()
async fn encode(&self, f_port: u8, variables: &HashMap<String, String>, obj: &prost_types::Struct) -> Result<Vec<u8>> {
let conf = config::get();
let max_run_ts = SystemTime::now() + conf.codec.js.max_execution_time;

let resolver = rquickjs::loader::BuiltinResolver::default()
.with_module("base64-js")
.with_module("ieee754")
.with_module("buffer");
let loader = rquickjs::loader::BuiltinLoader::default()
.with_module("base64-js", vendor_base64_js::SCRIPT)
.with_module("ieee754", vendor_ieee754::SCRIPT)
.with_module("buffer", vendor_buffer::SCRIPT);

let rt = rquickjs::Runtime::new()?;
rt.set_interrupt_handler(Some(Box::new(move || SystemTime::now() > max_run_ts)));
rt.set_loader(resolver, loader);

let ctx = rquickjs::Context::full(&rt)?;

ctx.with::<_, Result<Vec<u8>>>(|ctx| {
// We need to export the Buffer class, so it is correctly resolved
// in called encode/decode functions
let buff = rquickjs::Module::declare(
ctx.clone(),
"b",
r#"
import { Buffer } from "buffer";
export { Buffer }
"#,
)
.context("Declare script")?;
let (buff, buff_promise) = buff
.eval()
.catch(&ctx)
.map_err(|e| anyhow!("JS error: {}", e))?;
() = buff_promise.finish()?;
let buff: rquickjs::Function = buff.get("Buffer")?;

let m = rquickjs::Module::declare(ctx.clone(), "script", self.script.clone())
.context("Declare script")?;
let (m, m_promise) = m.eval().context("Evaluate script")?;
() = m_promise.finish()?;
let func: rquickjs::Function = m.get("encodeDownlink").context("Get encodeDownlink function")?;

let input = rquickjs::Object::new(ctx.clone())?;
input.set("fPort", f_port.into_js(&ctx)?)?;
input.set("variables", variables.into_js(&ctx)?)?;
input.set("data", convert::struct_to_rquickjs(&ctx, obj))?;

let globals = ctx.globals();
globals.set("Buffer", buff)?;

let res: rquickjs::Object = func
.call((input,))
.catch(&ctx)
.map_err(|e| anyhow!("JS error: {}", e))?;

let errors: Result<Vec<String>, rquickjs::Error> = res.get("errors");
if let Ok(errors) = errors {
if !errors.is_empty() {
return Err(anyhow!(
"encodeDownlink returned errors: {}",
errors.join(", ")
));
}
}

// Directly into u8 can result into the following error:
// Error converting from js 'float' into type 'i32'
let v: Vec<f64> = res.get("bytes")?;
let v: Vec<u8> = v.iter().map(|v| *v as u8).collect();

Ok(v)
})
}
}

async fn decode(&self, recv_time: DateTime<Utc>, f_port: u8, variables: &HashMap<String, String>, b: &[u8]) -> Result<pbjson_types::Struct> {
let conf = config::get();
let max_run_ts = SystemTime::now() + conf.codec.js.max_execution_time;

let resolver = rquickjs::loader::BuiltinResolver::default()
.with_module("base64-js")
.with_module("ieee754")
.with_module("buffer");
let loader = rquickjs::loader::BuiltinLoader::default()
.with_module("base64-js", vendor_base64_js::SCRIPT)
.with_module("ieee754", vendor_ieee754::SCRIPT)
.with_module("buffer", vendor_buffer::SCRIPT);

let rt = rquickjs::Runtime::new()?;
rt.set_interrupt_handler(Some(Box::new(move || SystemTime::now() > max_run_ts)));
rt.set_loader(resolver, loader);

let ctx = rquickjs::Context::full(&rt)?;

ctx.with::<_, Result<pbjson_types::Struct>>(|ctx| {
// We need to export the Buffer class, so it is correctly resolved
// in called encode/decode functions
let buff = rquickjs::Module::declare(
ctx.clone(),
"b",
r#"
import { Buffer } from "buffer";
export { Buffer }
"#,
)
.context("Declare script")?;
let (buff, buff_promise) = buff
.eval()
.catch(&ctx)
.map_err(|e| anyhow!("JS error: {}", e))?;
() = buff_promise.finish()?;
let buff: rquickjs::Function = buff.get("Buffer")?;

let m = rquickjs::Module::declare(ctx.clone(), "script", self.script.clone())
.context("Declare script")?;
let (m, m_promise) = m.eval().context("Evaluate script")?;
() = m_promise.finish()?;
let func: rquickjs::Function = m.get("decodeUplink").context("Get decodeUplink function")?;

let input = rquickjs::Object::new(ctx.clone())?;
input.set("bytes", b.into_js(&ctx)?)?;
input.set("fPort", f_port.into_js(&ctx)?)?;
input.set("recvTime", recv_time.into_js(&ctx)?)?;
input.set("variables", variables.into_js(&ctx)?)?;

let globals = ctx.globals();
globals.set("Buffer", buff)?;

let res: rquickjs::Object = func
.call((input,))
.catch(&ctx)
.map_err(|e| anyhow!("JS error: {}", e))?;

let errors: Result<Vec<String>, rquickjs::Error> = res.get("errors");
if let Ok(errors) = errors {
if !errors.is_empty() {
return Err(anyhow!(
"decodeUplink returned errors: {}",
errors.join(", ")
));
}
}

Ok(convert::rquickjs_to_struct(&res))
})
}
}

// TODO: add tests
8 changes: 2 additions & 6 deletions chirpstack/src/codec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,11 @@ pub async fn binary_to_struct(
codec_plugin_id: &str,
b: &[u8],
) -> Result<Option<pbjson_types::Struct>> {
let codec_plugin_script = js_plugin::get_plugin_script(codec_plugin_id).await;
Ok(match codec {
Codec::NONE => None,
Codec::CAYENNE_LPP => Some(cayenne_lpp::decode(b).context("CayenneLpp decode")?),
Codec::JS => Some(js::decode(recv_time, f_port, variables, decoder_config, b).await?),
// Call js::decode with codec plugin script
Codec::JS_PLUGIN => Some(js::decode(recv_time, f_port, variables, codec_plugin_script.as_str(), b).await?),
Codec::JS_PLUGIN => Some(js_plugin::decode(codec_plugin_id, recv_time, f_port, variables, b).await?),
})
}

Expand All @@ -106,13 +104,11 @@ pub async fn struct_to_binary(
codec_plugin_id: &str,
obj: &prost_types::Struct,
) -> Result<Vec<u8>> {
let codec_plugin_script = js_plugin::get_plugin_script(codec_plugin_id).await;
Ok(match codec {
Codec::NONE => Vec::new(),
Codec::CAYENNE_LPP => cayenne_lpp::encode(obj).context("CayenneLpp encode")?,
Codec::JS => js::encode(f_port, variables, encoder_config, obj).await?,
// Call js::decode with codec plugin script
Codec::JS_PLUGIN => js::encode(f_port, variables, codec_plugin_script.as_str(), obj).await?,
Codec::JS_PLUGIN => js_plugin::encode(codec_plugin_id, f_port, variables, obj).await?,
})
}

Expand Down
6 changes: 3 additions & 3 deletions examples/codec_plugins/plugin_skeleton.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function id() {
*
* @returns {{data: object}} Object representing the decoded payload.
*/
function decodeUplink(input) {
export function decodeUplink(input) {
return {
data: {
// temp: 22.5
Expand All @@ -35,8 +35,8 @@ function decodeUplink(input) {
*
* @returns {{bytes: number[]}} Byte array containing the downlink payload.
*/
function encodeDownlink(input) {
export function encodeDownlink(input) {
return {
bytes: input.data // Passthrough
};
}
}