Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# OpenWrt source tree cloned for local builds (99MB+, contains build artifacts)
openwrt/
3 changes: 1 addition & 2 deletions feeds/lorawan-gateway/rs485-module/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,5 @@ tokio-serial = "5.4"
rumqttc = { version = "0.24", features = ["websocket", "use-rustls"] }
rustls-pemfile = "2.1"
webpki-roots = "0.26"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = "0.4"
hex = "0.4"
6 changes: 3 additions & 3 deletions feeds/lorawan-gateway/rs485-module/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ define Package/rs485-module
endef

define Package/rs485-module/description
Bidirectional bridge between RS485 serial port and MQTT broker.
Bidirectional transparent bridge between RS485 serial port and MQTT broker.
Written in Rust with rumqttc for async MQTT communication.
Uplink: RS485 data wrapped in JSON {"data":"..."}.
Downlink: MQTT JSON {"data":"..."} converted to raw bytes.
Uplink: RS485 raw bytes published directly to rs485/CH<N>/uplink.
Downlink: payload of rs485/CH<N>/downlink written directly to /dev/RS485-<N>.
Reads configuration from UCI (/etc/config/rs485-module).
endef

Expand Down
Original file line number Diff line number Diff line change
@@ -1,59 +1,29 @@
#!/bin/sh

# Create rs485-module config if not exists
# Create per-port rs485-module config if the file does not exist.
# Field names here MUST match what main.rs (rs485-module.portN.*) reads.
if [ ! -e "/etc/config/rs485-module" ]; then
touch /etc/config/rs485-module

# mqtt section
uci set rs485-module.mqtt=mqtt
uci set rs485-module.mqtt.enabled='0'
uci set rs485-module.mqtt.transport='tcp'
uci set rs485-module.mqtt.host=''
uci set rs485-module.mqtt.port='1883'
uci set rs485-module.mqtt.client_id=''
uci set rs485-module.mqtt.keepalive='30'
uci set rs485-module.mqtt.username=''
uci set rs485-module.mqtt.password=''
uci set rs485-module.mqtt.auth_mode='none'
uci set rs485-module.mqtt.uplink_topic='rs485/uplink'
uci set rs485-module.mqtt.downlink_topic='rs485/downlink'
uci set rs485-module.mqtt.clean_session='1'
uci set rs485-module.mqtt.qos='0'
uci set rs485-module.mqtt.reconnect_delay='30'
uci set rs485-module.mqtt.retain_status='0'

# serial section
uci set rs485-module.serial=serial
uci set rs485-module.serial.enabled='1'
uci set rs485-module.serial.device='RS485-1'
uci set rs485-module.serial.baudrate='9600'
uci set rs485-module.serial.databit='8'
uci set rs485-module.serial.stopbit='1'
uci set rs485-module.serial.checkbit='none'
uci set rs485-module.serial.flowcontrol='none'
uci set rs485-module.serial.timeout='1000'

# log section
uci set rs485-module.ui=log
uci set rs485-module.ui.auto_refresh='1'
uci set rs485-module.ui.buffer_limit='2000'
uci add_list rs485-module.ui.watch_keywords='join'
uci add_list rs485-module.ui.watch_keywords='error'

# protocol section
uci set rs485-module.protocol=protocol
uci set rs485-module.protocol.enabled='0'
uci set rs485-module.protocol.type='modbus-rtu'
uci set rs485-module.protocol.device_address='1'
uci set rs485-module.protocol.function_code='03'
uci set rs485-module.protocol.work_mode='once'
uci set rs485-module.protocol.register_address='40001'
uci set rs485-module.protocol.data_length='10'
uci set rs485-module.protocol.poll_interval='3'
uci set rs485-module.protocol.timeout='10'
uci set rs485-module.protocol.enable_crc='1'
uci set rs485-module.protocol.write_value='0'
uci set rs485-module.protocol.standard_mode='1'
for n in 1 2 3; do
sid="port$n"
uci set "rs485-module.${sid}=port"
uci set "rs485-module.${sid}.baudrate='9600'"
uci set "rs485-module.${sid}.databit='8'"
uci set "rs485-module.${sid}.stopbit='1'"
uci set "rs485-module.${sid}.checkbit='none'"
uci set "rs485-module.${sid}.flowcontrol='none'"
uci set "rs485-module.${sid}.timeout='1000'"
uci set "rs485-module.${sid}.mqtt_transport='tcp'"
uci set "rs485-module.${sid}.mqtt_host=''"
uci set "rs485-module.${sid}.mqtt_port='1883'"
uci set "rs485-module.${sid}.mqtt_keepalive='30'"
uci set "rs485-module.${sid}.mqtt_auth_mode='none'"
uci set "rs485-module.${sid}.mqtt_qos='0'"
uci set "rs485-module.${sid}.mqtt_reconnect_delay='5'"
uci set "rs485-module.${sid}.mqtt_uplink_topic='rs485/CH${n}/uplink'"
uci set "rs485-module.${sid}.mqtt_downlink_topic='rs485/CH${n}/downlink'"
done

uci commit rs485-module
fi
Expand Down
11 changes: 4 additions & 7 deletions feeds/lorawan-gateway/rs485-module/files/rs485-module.init
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,13 @@ USE_PROCD=1
PROG=/usr/bin/rs485-module

start_service() {
local serial_enabled port_num protocol_enabled

serial_enabled=$(uci -q get rs485-module.serial.enabled)
[ "$serial_enabled" = "1" ] || return 0
local port_num protocol_enabled

for port_num in 1 2 3; do
protocol_enabled=$(uci -q get rs485-module.port${port_num}.protocol_enabled)
protocol_enabled=$(uci -q get "rs485-module.port${port_num}.protocol_enabled")

# Only start transparent mode for ports where protocol is not enabled
[ "$protocol_enabled" != "1" ] || continue
# Skip transparent mode for ports claimed by a protocol handler (e.g. BACnet MS/TP).
[ "$protocol_enabled" = "1" ] && continue

procd_open_instance "port${port_num}"
procd_set_param command $PROG "$port_num"
Expand Down
60 changes: 17 additions & 43 deletions feeds/lorawan-gateway/rs485-module/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use chrono::Local;
use rumqttc::{AsyncClient, Event, Incoming, MqttOptions, QoS, Transport};
use rumqttc::tokio_rustls::rustls::ClientConfig as RustlsClientConfig;
use rustls_pemfile::{certs, pkcs8_private_keys};
use serde::{Deserialize, Serialize};
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::process::Command;
Expand Down Expand Up @@ -76,18 +75,6 @@ struct SerialConfig {
timeout: Duration,
}

// RS485 -> MQTT Message Structure
#[derive(Debug, Serialize)]
struct UplinkMessage {
data: String,
}

// MQTT -> RS485 Message Structure
#[derive(Debug, Deserialize)]
struct DownlinkMessage {
data: String,
}

// Logger Structure
struct Logger {
file: StdMutex<Option<File>>,
Expand Down Expand Up @@ -552,29 +539,24 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
logger.log(&format!("Subscribed [MQTT->RS485] to topic: {}", config.mqtt.downlink_topic));
}
Err(e) => {
logger.log(&format!("Failed to topic: {}", e));
logger.log(&format!("Failed to subscribe: {}", e));
}
}
}
logger.log(&format!("Published [RS485->MQTT] to topic: {}", config.mqtt.uplink_topic));
logger.log(&format!("MQTT connected, uplink ready on: {}", config.mqtt.uplink_topic));
}
// Handle incoming publish messages
Incoming::Publish(p) => {
let payload = String::from_utf8_lossy(&p.payload);
logger.log(&format!("MQTT received: {}", payload));

if let Ok(msg) = serde_json::from_str::<DownlinkMessage>(&payload) {
let data = msg.data.as_bytes();
match AsyncWriteExt::write_all(&mut serial_port, data).await {
Ok(_) => {
logger.log(&format!("Forwarded to RS485: {}", msg.data));
}
Err(e) => {
logger.log(&format!("RS485 write failed: {}", e));
}
let payload_hex = hex::encode_upper(&p.payload);
logger.log(&format!("MQTT received ({} bytes): {}", p.payload.len(), payload_hex));

match AsyncWriteExt::write_all(&mut serial_port, &p.payload).await {
Ok(_) => {
logger.log(&format!("Forwarded to RS485: {}", payload_hex));
}
Err(e) => {
logger.log(&format!("RS485 write failed: {}", e));
}
} else {
logger.log(&format!("Invalid message format, expected {{\"data\":\"...\"}}, got: {}", payload));
}
}
// Handle subscription acknowledgment
Expand Down Expand Up @@ -627,24 +609,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
} => {
match serial_result {
Ok((n, buffer)) if n > 0 => {
let data_str = String::from_utf8_lossy(&buffer[..n]).trim().to_string();
logger.log(&format!("RS485 received ({} bytes): {}", n, data_str));
let payload_hex = hex::encode_upper(&buffer[..n]);
logger.log(&format!("RS485 received ({} bytes): {}", n, payload_hex));

if let Some(ref client) = mqtt_client {
let uplink_msg = UplinkMessage { data: data_str.clone() };
match serde_json::to_string(&uplink_msg) {
Ok(json) => {
match client.publish(&config.mqtt.uplink_topic, config.mqtt.qos_level, false, json.as_bytes()).await {
Ok(_) => {
logger.log(&format!("Published to MQTT: {}", json));
}
Err(e) => {
logger.log(&format!("MQTT publish failed: {}", e));
}
}
match client.publish(&config.mqtt.uplink_topic, config.mqtt.qos_level, false, &buffer[..n]).await {
Ok(_) => {
logger.log(&format!("Published to MQTT: {}", payload_hex));
}
Err(e) => {
logger.log(&format!("JSON serialization failed: {}", e));
logger.log(&format!("MQTT publish failed: {}", e));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ return view.extend({
logView.renderWidget = function(section_id, option_id, cfgvalue) {
return E('div', {
'id': 'log_view',
'style': 'width:100%; height:600px; overflow:auto; border:1px solid #ccc; padding:5px; font-family:monospace; white-space:pre; background:#fff; color:#333; resize:vertical;'
'style': 'width:100%; height:600px; overflow:auto; border:1px solid var(--sensecap-log-border); padding:5px; font-family:monospace; white-space:pre; background:var(--sensecap-log-bg); color:var(--sensecap-log-fg); resize:vertical;'
}, _('Loading logs...'));
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ return view.extend({
if (!document.getElementById('basicstation-sfu-style')) {
var style = document.createElement('style');
style.id = 'basicstation-sfu-style';
style.textContent = '.sfu-container{border-radius:10px;padding:16px}.sfu-dropzone{border:2px dashed #cbd5e0;border-radius:6px;padding:16px;text-align:center;cursor:pointer;transition:border-color 0.2s,background-color 0.2s}.sfu-dropzone:hover{border-color:#8FC320;background:#f3f8e8}.sfu-dropzone.has-file{border-style:solid;border-color:#8FC320}.sfu-text{color:#6b7280;font-size:15px}.sfu-dropzone.has-file .sfu-text{color:#2d3748;font-weight:500}.sfu-buttons{display:flex;justify-content:flex-end;gap:10px;margin-top:14px;padding-top:14px;border-top:1px solid #e2e8f0}.sfu-btn-remove,.sfu-btn-upload{color:#fff!important;background:#8898aa;border:none!important;padding:6px 20px;border-radius:4px;font-size:14px;cursor:pointer}.sfu-btn-upload:hover,.sfu-btn-remove:hover{background:#6c7a8d}';
style.textContent = '.sfu-container{border-radius:10px;padding:16px}.sfu-dropzone{border:2px dashed var(--sensecap-dropzone-border);border-radius:6px;padding:16px;text-align:center;cursor:pointer;transition:border-color 0.2s,background-color 0.2s}.sfu-dropzone:hover{border-color:var(--primary);background:var(--sensecap-dropzone-hover-bg)}.sfu-dropzone.has-file{border-style:solid;border-color:var(--primary)}.sfu-text{color:var(--sensecap-muted-fg);font-size:15px}.sfu-dropzone.has-file .sfu-text{color:var(--sensecap-emphasis-fg);font-weight:500}.sfu-buttons{display:flex;justify-content:flex-end;gap:10px;margin-top:14px;padding-top:14px;border-top:1px solid var(--sensecap-divider)}.sfu-btn-remove,.sfu-btn-upload{color:#fff!important;background:var(--sensecap-neutral-btn-bg);border:none!important;padding:6px 20px;border-radius:4px;font-size:14px;cursor:pointer}.sfu-btn-upload:hover,.sfu-btn-remove:hover{background:var(--sensecap-neutral-btn-bg-hover)}';
document.head.appendChild(style);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ return baseclass.extend({

rv.appendChild(E('div', { 'class': 'cbi-section' }, [
E('h3', _('LoRa Packets')),
E('div', { 'style': 'background:#fff; margin: 20px 0; border:1px solid #ddd;' }, [svg2Div])
E('div', { 'style': 'background:var(--sensecap-chart-bg); margin: 20px 0; border:1px solid var(--sensecap-chart-border);' }, [svg2Div])
]));

return rv;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ return view.extend({
var self = this;
ui.showModal(_('OTA Firmware Upgrade'), [
E('div', {}, 'Ready to download firmware from:'),
E('pre', { 'style': 'margin: 10px 0; padding: 5px; background: #f5f5f5; overflow-x: auto;' }, displayUrl),
E('pre', { 'style': 'margin: 10px 0; padding: 5px; background: var(--sensecap-muted-bg); color: var(--sensecap-log-fg); overflow-x: auto;' }, displayUrl),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'btn',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ return view.extend({
logView.renderWidget = function(section_id, option_id, cfgvalue) {
return E('div', {
'id': 'log_view',
'style': 'width:100%; height:600px; overflow:auto; border:1px solid #ccc; padding:5px; font-family:monospace; white-space:pre; background:#fff; color:#333; resize:vertical;'
'style': 'width:100%; height:600px; overflow:auto; border:1px solid var(--sensecap-log-border); padding:5px; font-family:monospace; white-space:pre; background:var(--sensecap-log-bg); color:var(--sensecap-log-fg); resize:vertical;'
}, _('Loading logs...'));
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ return view.extend({
logView.renderWidget = function(section_id, option_id, cfgvalue) {
return E('div', {
'id': 'log_view',
'style': 'width:100%; height:600px; overflow:auto; border:1px solid #ccc; padding:5px; font-family:monospace; white-space:pre; background:#fff; color:#333; resize:vertical;'
'style': 'width:100%; height:600px; overflow:auto; border:1px solid var(--sensecap-log-border); padding:5px; font-family:monospace; white-space:pre; background:var(--sensecap-log-bg); color:var(--sensecap-log-fg); resize:vertical;'
}, _('Loading logs...'));
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ return view.extend({
logView.renderWidget = function(section_id, option_id, cfgvalue) {
return E('div', {
'id': 'ups_log_view',
'style': 'width:100%; height:400px; overflow:auto; border:1px solid #ccc; padding:10px; font-family:monospace; white-space:pre; background:#f5f5f5; color:#333; resize:vertical;'
'style': 'width:100%; height:400px; overflow:auto; border:1px solid var(--sensecap-log-border); padding:10px; font-family:monospace; white-space:pre; background:var(--sensecap-muted-bg); color:var(--sensecap-log-fg); resize:vertical;'
}, _('Loading logs...'));
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@
--blur-opacity-dark: 0.5;
--font-family-sans-serif: "Google Sans", "Microsoft Yahei", "WenQuanYi Micro Hei", "sans-serif", "Helvetica Neue", "Helvetica", "Hiragino Sans GB";
--dropdown-arrow-icon: url("data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik01LjI5MyA5LjI5M2ExIDEgMCAwIDEgMS40MTQgMEwxMiAxNC41ODZsNS4yOTMtNS4yOTNhMSAxIDAgMSAxIDEuNDE0IDEuNDE0bC02IDZhMSAxIDAgMCAxLTEuNDE0IDBsLTYtNmExIDEgMCAwIDEgMC0xLjQxNCIvPjwvc3ZnPg==");

/* SenseCAP tokens consumed by luci-app-* inline styles — overridden in dark.css */
--sensecap-log-bg: #ffffff;
--sensecap-log-fg: #333333;
--sensecap-log-border: #cccccc;
--sensecap-chart-bg: #ffffff;
--sensecap-chart-border: #dddddd;
--sensecap-muted-bg: #f5f5f5;
--sensecap-dropzone-border: #cbd5e0;
--sensecap-dropzone-hover-bg: #f3f8e8;
--sensecap-muted-fg: #6b7280;
--sensecap-emphasis-fg: #2d3748;
--sensecap-divider: #e2e8f0;
--sensecap-neutral-btn-bg: #8898aa;
--sensecap-neutral-btn-bg-hover: #6c7a8d;
}
/*
* Include base and custom css
Expand Down

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions feeds/luci-lorawan-gateway/luci-theme-sensecap/less/cascade.less
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@
--blur-opacity-dark: 0.5;
--font-family-sans-serif: "Google Sans", "Microsoft Yahei", "WenQuanYi Micro Hei", "sans-serif", "Helvetica Neue", "Helvetica", "Hiragino Sans GB";
--dropdown-arrow-icon: url("data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik01LjI5MyA5LjI5M2ExIDEgMCAwIDEgMS40MTQgMEwxMiAxNC41ODZsNS4yOTMtNS4yOTNhMSAxIDAgMSAxIDEuNDE0IDEuNDE0bC02IDZhMSAxIDAgMCAxLTEuNDE0IDBsLTYtNmExIDEgMCAwIDEgMC0xLjQxNCIvPjwvc3ZnPg==");

/* SenseCAP tokens consumed by luci-app-* inline styles — overridden in dark.css */
--sensecap-log-bg: #ffffff;
--sensecap-log-fg: #333333;
--sensecap-log-border: #cccccc;
--sensecap-chart-bg: #ffffff;
--sensecap-chart-border: #dddddd;
--sensecap-muted-bg: #f5f5f5;
--sensecap-dropzone-border: #cbd5e0;
--sensecap-dropzone-hover-bg: #f3f8e8;
--sensecap-muted-fg: #6b7280;
--sensecap-emphasis-fg: #2d3748;
--sensecap-divider: #e2e8f0;
--sensecap-neutral-btn-bg: #8898aa;
--sensecap-neutral-btn-bg-hover: #6c7a8d;
}

/*
Expand Down
17 changes: 17 additions & 0 deletions feeds/luci-lorawan-gateway/luci-theme-sensecap/less/dark.less
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@ body {
color: #cccccc;
}

/* SenseCAP dark-mode token overrides (consumed by luci-app-* inline styles) */
:root {
--sensecap-log-bg: #1e1e1e;
--sensecap-log-fg: #cccccc;
--sensecap-log-border: #3c3c3c;
--sensecap-chart-bg: #1e1e1e;
--sensecap-chart-border: #252526;
--sensecap-muted-bg: #252526;
--sensecap-dropzone-border: #3c3c3c;
--sensecap-dropzone-hover-bg: #2a2e1a;
--sensecap-muted-fg: #adb5bd;
--sensecap-emphasis-fg: #ced4da;
--sensecap-divider: #3c3c3c;
--sensecap-neutral-btn-bg: #4a5568;
--sensecap-neutral-btn-bg-hover: #5a6578;
}

.spinning::before {
filter: invert(1);
}
Expand Down