Add silent mode for sensor detection and moisture measurement

- Introduced the `silent` parameter to prevent UI progress updates during automatic operations.
- Enhanced CAN robustness with improved bus-off management, retransmission settings, and jitter tolerance.
- Added auto-refresh functionality for plant moisture and sensor detection with configurable enablement.
This commit is contained in:
2026-03-29 14:21:12 +02:00
parent 4cf5f6d151
commit 7121dd0fae
4 changed files with 274 additions and 194 deletions

View File

@@ -248,6 +248,23 @@ async fn main(spawner: Spawner) {
ch32_hal::pac::AFIO.pcfr1().write(|w| w.set_can1_rm(2)); ch32_hal::pac::AFIO.pcfr1().write(|w| w.set_can1_rm(2));
can.add_filter(CanFilter::accept_all()); can.add_filter(CanFilter::accept_all());
// Improve CAN robustness for longer cables:
// 1. Enable Automatic Bus-Off Management (ABOM)
// 2. Ensure No Automatic Retransmission (NART) is DISABLED (i.e. we WANT retransmission)
// 3. Enable Receive FIFO Overwrite Mode (RFLM = 0, default)
// 4. Increase Resync Jump Width (SJW) if possible by patching BTIMR
hal::pac::CAN1.ctlr().modify(|w| {
w.set_abom(true);
w.set_nart(false); // HAL default is usually false, but let's be explicit
});
// SJW is bits 24-25 of BTIMR. HAL sets it to 0 (SJW=1).
// Let's try to set it to 3 (SJW=4) for better jitter tolerance.
hal::pac::CAN1.btimr().modify(|w| {
w.set_sjw(3); // 3 means 4TQ
});
// let mut filter = CanFilter::new_id_list(); // let mut filter = CanFilter::new_id_list();
// filter.get(0).unwrap().set(Id::Standard(standard_identify_id), Default::default()); // filter.get(0).unwrap().set(Id::Standard(standard_identify_id), Default::default());
// can.add_filter(filter); // can.add_filter(filter);

View File

@@ -1,195 +1,202 @@
<style> <style>
.progressPane { .progressPane {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: fixed; position: fixed;
width: 100%; width: 100%;
height: 100%; height: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
top: 0; top: 0;
left: 0; left: 0;
background-color: grey; background-color: grey;
opacity: 0.8; opacity: 0.8;
}
.progress {
height: 2.5em;
width: 100%;
background-color: #555;
position: relative;
}
.progressSpacer{
flex-grow: 1;
}
.progress:after {
content: attr(data-label);
font-size: 0.8em;
position: absolute;
text-align: center;
top: 10px;
left: 0;
right: 0;
}
.progress .value {
background-color: darkcyan;
display: inline-block;
height: 100%;
}
.progress .valueIndeterminate {
background-color: darkcyan;
display: inline-block;
height: 100%;
animation: indeterminateAnimation 1s infinite linear;
transform-origin: 0 50%;
}
@keyframes indeterminateAnimation {
0% {
transform: translateX(0%) scaleX(0.5);
} }
50% { .progress {
transform: translateX(50%) scaleX(0.5); height: 2.5em;
width: 100%;
background-color: #555;
position: relative;
} }
100% { .progressSpacer {
transform: translateX(0%) scaleX(0.5); flex-grow: 1;
} }
}
.progress:after {
.flexcontainer { content: attr(data-label);
display: flex; font-size: 0.8em;
flex-wrap: wrap; position: absolute;
} text-align: center;
.flexcontainer-rev{ top: 10px;
display: flex; left: 0;
flex-wrap: wrap-reverse; right: 0;
}
.subcontainer {
min-width: 300px;
max-width: 900px;
flex-grow: 1;
border-style: solid;
border-width: 1px;
padding: 8px;
}
.subcontainercontainer{
flex-grow: 1;
}
.plantcontainer {
flex-grow: 1;
min-width: 100%;
border-style: solid;
border-width: 1px;
padding: 8px;
} }
@media (min-width: 350px) {
.progress .value {
background-color: darkcyan;
display: inline-block;
height: 100%;
}
.progress .valueIndeterminate {
background-color: darkcyan;
display: inline-block;
height: 100%;
animation: indeterminateAnimation 1s infinite linear;
transform-origin: 0 50%;
}
@keyframes indeterminateAnimation {
0% {
transform: translateX(0%) scaleX(0.5);
}
50% {
transform: translateX(50%) scaleX(0.5);
}
100% {
transform: translateX(0%) scaleX(0.5);
}
}
.flexcontainer {
display: flex;
flex-wrap: wrap;
}
.flexcontainer-rev {
display: flex;
flex-wrap: wrap-reverse;
}
.subcontainer {
min-width: 300px;
max-width: 900px;
flex-grow: 1;
border-style: solid;
border-width: 1px;
padding: 8px;
}
.subcontainercontainer {
flex-grow: 1;
}
.plantcontainer { .plantcontainer {
flex-grow: 1; flex-grow: 1;
min-width: 40%; min-width: 100%;
border-style: solid; border-style: solid;
border-width: 1px; border-width: 1px;
padding: 8px; padding: 8px;
} }
}
@media (min-width: 1100px) { @media (min-width: 350px) {
.plantcontainer { .plantcontainer {
flex-grow: 1; flex-grow: 1;
min-width: 20%; min-width: 40%;
border-style: solid; border-style: solid;
border-width: 1px; border-width: 1px;
padding: 8px; padding: 8px;
}
} }
}
@media (min-width: 2150px) { @media (min-width: 1100px) {
.plantcontainer { .plantcontainer {
flex-grow: 1; flex-grow: 1;
min-width: 200px; min-width: 20%;
border-style: solid; border-style: solid;
border-width: 1px; border-width: 1px;
padding: 8px; padding: 8px;
}
}
@media (min-width: 2150px) {
.plantcontainer {
flex-grow: 1;
min-width: 200px;
border-style: solid;
border-width: 1px;
padding: 8px;
}
} }
}
.plantlist {
display: flex;
flex-wrap: wrap;
}
.plantlist { .subtitle {
display: flex; flex-grow: 1;
flex-wrap: wrap; text-align: center;
} font-weight: bold;
}
.subtitle {
flex-grow: 1;
text-align: center;
font-weight: bold;
}
</style> </style>
<div class="container-xl"> <div class="container-xl">
<div style="display:flex; flex-wrap: wrap;"> <div style="display:flex; flex-wrap: wrap;">
<div id="hardwareview" class="subcontainer"></div> <div id="hardwareview" class="subcontainer"></div>
</div>
<div style="display:flex; flex-wrap: wrap;">
<div id="firmwareview" class="subcontainer">
</div> </div>
<div id="timeview" class="subcontainer">
<div style="display:flex; flex-wrap: wrap;">
<div id="firmwareview" class="subcontainer">
</div>
<div id="timeview" class="subcontainer">
</div>
<div id="batteryview" class="subcontainer">
</div>
<div id="solarview" class="subcontainer">
</div>
</div> </div>
<div id="batteryview" class="subcontainer">
<div class="flexcontainer">
<div id="network_view" class="subcontainercontainer"></div>
<div id="lightview" class="subcontainer">
</div>
<div id="tankview" class="subcontainer">
</div>
</div> </div>
<div id="solarview" class="subcontainer">
<h3>Plants:</h3>
<button id="measure_moisture">Measure Moisture</button>
<button id="detect_sensors" style="display:none">Detect/Test Sensors</button>
<input id="can_power" type="checkbox"><label for="can_power">Power CAN</label>
<input id="auto_refresh_moisture_sensors" type="checkbox"><label for="auto_refresh_moisture_sensors">Auto Refresh
Moisture/Sensors</label>
<div id="plants" class="plantlist"></div>
<div class="flexcontainer-rev">
<div id="submitview" class="subcontainer">
</div>
<div id="fileview" class="subcontainer">
</div>
</div> </div>
</div>
<div class="flexcontainer">
<div id="network_view" class="subcontainercontainer"></div> <button id="exit">Exit</button>
<div id="lightview" class="subcontainer"> <button id="reboot">Reboot</button>
<div class="flexcontainer">
<div id="logview" class="subcontainercontainer"></div>
</div> </div>
<div id="tankview" class="subcontainer">
</div>
</div>
<h3>Plants:</h3> <script src="bundle.js"></script>
<button id="measure_moisture">Measure Moisture</button>
<button id="detect_sensors" style="display:none">Detect/Test Sensors</button>
<input id="can_power" type="checkbox">Power CAN</input>
<div id="plants" class="plantlist"></div>
<div class="flexcontainer-rev">
<div id = "submitview" class="subcontainer">
</div>
<div id="fileview" class="subcontainer">
</div>
</div>
<button id="exit">Exit</button>
<button id="reboot">Reboot</button>
<div class="flexcontainer">
<div id="logview" class="subcontainercontainer"></div>
</div>
<script src="bundle.js"></script>
</div> </div>
<div id="progressPane" class="progressPane"> <div id="progressPane" class="progressPane">
<div class="progressSpacer"></div>> <div class="progressSpacer"></div>
<div id="progressPaneBar" class="progress" data-label="50% Complete"> >
<span id="progressPaneSpan" class="value" style="width:100%;"></span> <div id="progressPaneBar" class="progress" data-label="50% Complete">
</div> <span id="progressPaneSpan" class="value" style="width:100%;"></span>
<div class="progressSpacer"></div>> </div>
<div class="progressSpacer"></div>
>
</div> </div>

View File

@@ -362,16 +362,21 @@ export class Controller {
) )
} }
async detectSensors(detection: Detection) { async detectSensors(detection: Detection, silent: boolean = false) {
let counter = 0 let counter = 0
let limit = 5 let limit = 5
controller.progressview.addProgress("detect_sensors", counter / limit * 100, "Detecting sensors " + (limit - counter) + "s") if (!silent) {
controller.progressview.addProgress("detect_sensors", counter / limit * 100, "Detecting sensors " + (limit - counter) + "s")
}
let timerId: string | number | NodeJS.Timeout | undefined let timerId: string | number | NodeJS.Timeout | undefined
function updateProgress() { function updateProgress() {
counter++; counter++;
controller.progressview.addProgress("detect_sensors", counter / limit * 100, "Detecting sensors " + (limit - counter) + "s") if (!silent) {
controller.progressview.addProgress("detect_sensors", counter / limit * 100, "Detecting sensors " + (limit - counter) + "s")
}
timerId = setTimeout(updateProgress, 1000); timerId = setTimeout(updateProgress, 1000);
} }
@@ -379,12 +384,15 @@ export class Controller {
var pretty = JSON.stringify(detection, undefined, 1); var pretty = JSON.stringify(detection, undefined, 1);
fetch(PUBLIC_URL + "/detect_sensors", {method: "POST", body: pretty}) return fetch(PUBLIC_URL + "/detect_sensors", {method: "POST", body: pretty})
.then(response => response.json()) .then(response => response.json())
.then(json => json as Detection) .then(json => json as Detection)
.then(json => { .then(json => {
clearTimeout(timerId); clearTimeout(timerId);
controller.progressview.removeProgress("detect_sensors"); if (!silent) {
controller.progressview.removeProgress("detect_sensors");
}
const pretty = JSON.stringify(json); const pretty = JSON.stringify(json);
toast.info("Detection result: " + pretty); toast.info("Detection result: " + pretty);
console.log(pretty); console.log(pretty);
@@ -393,7 +401,9 @@ export class Controller {
}) })
.catch(error => { .catch(error => {
clearTimeout(timerId); clearTimeout(timerId);
controller.progressview.removeProgress("detect_sensors"); if (!silent) {
controller.progressview.removeProgress("detect_sensors");
}
toast.error("Autodetect failed: " + error); toast.error("Autodetect failed: " + error);
}); });
} }
@@ -426,7 +436,7 @@ export class Controller {
timerId = setTimeout(updateProgress, 1000); timerId = setTimeout(updateProgress, 1000);
var ajax = new XMLHttpRequest(); const ajax = new XMLHttpRequest();
ajax.responseType = 'json'; ajax.responseType = 'json';
ajax.onreadystatechange = () => { ajax.onreadystatechange = () => {
if (ajax.readyState === 4) { if (ajax.readyState === 4) {
@@ -435,7 +445,7 @@ export class Controller {
this.networkView.setScanResult(ajax.response as SSIDList) this.networkView.setScanResult(ajax.response as SSIDList)
} }
}; };
ajax.onerror = (evt) => { ajax.onerror = (_) => {
clearTimeout(timerId); clearTimeout(timerId);
controller.progressview.removeProgress("scan_ssid"); controller.progressview.removeProgress("scan_ssid");
alert("Failed to start see console") alert("Failed to start see console")
@@ -459,16 +469,22 @@ export class Controller {
this.hardwareView.setConfig(current.hardware); this.hardwareView.setConfig(current.hardware);
} }
measure_moisture() { measure_moisture(silent: boolean = false) {
let counter = 0 let counter = 0
let limit = 2 let limit = 2
controller.progressview.addProgress("measure_moisture", counter / limit * 100, "Measure Moisture " + (limit - counter) + "s") if (!silent) {
controller.progressview.addProgress("measure_moisture", counter / limit * 100, "Measure Moisture " + (limit - counter) + "s")
}
let timerId: string | number | NodeJS.Timeout | undefined let timerId: string | number | NodeJS.Timeout | undefined
function updateProgress() { function updateProgress() {
counter++; counter++;
controller.progressview.addProgress("measure_moisture", counter / limit * 100, "Measure Moisture " + (limit - counter) + "s") if (!silent) {
controller.progressview.addProgress("measure_moisture", counter / limit * 100, "Measure Moisture " + (limit - counter) + "s")
}
timerId = setTimeout(updateProgress, 1000); timerId = setTimeout(updateProgress, 1000);
} }
@@ -476,17 +492,22 @@ export class Controller {
timerId = setTimeout(updateProgress, 1000); timerId = setTimeout(updateProgress, 1000);
fetch(PUBLIC_URL + "/moisture") return fetch(PUBLIC_URL + "/moisture")
.then(response => response.json()) .then(response => response.json())
.then(json => json as Moistures) .then(json => json as Moistures)
.then(time => { .then(time => {
controller.plantViews.update(time.moisture_a, time.moisture_b) controller.plantViews.update(time.moisture_a, time.moisture_b)
clearTimeout(timerId); clearTimeout(timerId);
controller.progressview.removeProgress("measure_moisture"); if (!silent) {
controller.progressview.removeProgress("measure_moisture");
}
}) })
.catch(error => { .catch(error => {
clearTimeout(timerId); clearTimeout(timerId);
controller.progressview.removeProgress("measure_moisture"); if (!silent) {
controller.progressview.removeProgress("measure_moisture");
}
console.log(error); console.log(error);
}); });
} }
@@ -516,7 +537,7 @@ export class Controller {
}, 2000); }, 2000);
} }
}) })
.catch(err => { .catch(_ => {
console.log("Not reached yet, retrying") console.log("Not reached yet, retrying")
setTimeout(controller.waitForReboot, 1000) setTimeout(controller.waitForReboot, 1000)
}) })
@@ -560,6 +581,8 @@ export class Controller {
readonly logView: LogView readonly logView: LogView
readonly detectBtn: HTMLButtonElement readonly detectBtn: HTMLButtonElement
readonly can_power: HTMLInputElement; readonly can_power: HTMLInputElement;
readonly auto_refresh_moisture_sensors: HTMLInputElement;
private auto_refresh_timer: NodeJS.Timeout | undefined;
constructor() { constructor() {
this.timeView = new TimeView(this) this.timeView = new TimeView(this)
@@ -597,6 +620,38 @@ export class Controller {
this.can_power.onchange = () => { this.can_power.onchange = () => {
controller.setCanPower(this.can_power.checked); controller.setCanPower(this.can_power.checked);
} }
this.auto_refresh_moisture_sensors = document.getElementById("auto_refresh_moisture_sensors") as HTMLInputElement
this.auto_refresh_moisture_sensors.onchange = () => {
if (this.auto_refresh_timer) {
clearTimeout(this.auto_refresh_timer)
}
if (this.auto_refresh_moisture_sensors.checked) {
this.autoRefreshLoop()
}
}
}
private async autoRefreshLoop() {
if (!this.auto_refresh_moisture_sensors.checked) {
return;
}
try {
await this.measure_moisture(true);
const detection: Detection = {
plant: Array.from({length: PLANT_COUNT}, () => ({
sensor_a: true,
sensor_b: true,
})),
};
await this.detectSensors(detection, true);
} catch (e) {
console.error("Auto-refresh error", e);
}
if (this.auto_refresh_moisture_sensors.checked) {
this.auto_refresh_timer = setTimeout(() => this.autoRefreshLoop(), 1000);
}
} }

View File

@@ -12,7 +12,9 @@ export class PlantViews {
constructor(syncConfig: Controller) { constructor(syncConfig: Controller) {
this.measure_moisture = document.getElementById("measure_moisture") as HTMLButtonElement this.measure_moisture = document.getElementById("measure_moisture") as HTMLButtonElement
this.measure_moisture.onclick = syncConfig.measure_moisture this.measure_moisture.onclick = async () => {
return syncConfig.measure_moisture(false)
}
this.plantsDiv = document.getElementById("plants") as HTMLDivElement; this.plantsDiv = document.getElementById("plants") as HTMLDivElement;
for (let plantId = 0; plantId < PLANT_COUNT; plantId++) { for (let plantId = 0; plantId < PLANT_COUNT; plantId++) {
this.plants[plantId] = new PlantView(plantId, this.plantsDiv, syncConfig); this.plants[plantId] = new PlantView(plantId, this.plantsDiv, syncConfig);
@@ -48,16 +50,15 @@ export class PlantViews {
plantView.setTestResult(response) plantView.setTestResult(response)
} }
applyDetectionResult(json: Detection) { applyDetectionResult(json: Detection) {
for (let i = 0; i < PLANT_COUNT; i++) { for (let i = 0; i < PLANT_COUNT; i++) {
var plantResult = json.plant[i]; var plantResult = json.plant[i];
this.plants[i].setDetectionResult(plantResult); this.plants[i].setDetectionResult(plantResult);
} }
} }
} }
export class PlantView { export class PlantView {
private readonly moistureSensorMinFrequency: HTMLInputElement; private readonly moistureSensorMinFrequency: HTMLInputElement;
private readonly moistureSensorMaxFrequency: HTMLInputElement; private readonly moistureSensorMaxFrequency: HTMLInputElement;
@@ -240,10 +241,10 @@ export class PlantView {
} }
updateVisibility(plantConfig: PlantConfig) { updateVisibility(plantConfig: PlantConfig) {
let sensorOnly = document.getElementsByClassName("plantSensorEnabledOnly_"+ this.plantId) let sensorOnly = document.getElementsByClassName("plantSensorEnabledOnly_" + this.plantId)
let pumpOnly = document.getElementsByClassName("plantPumpEnabledOnly_"+ this.plantId) let pumpOnly = document.getElementsByClassName("plantPumpEnabledOnly_" + this.plantId)
let targetOnly = document.getElementsByClassName("plantTargetEnabledOnly_"+ this.plantId) let targetOnly = document.getElementsByClassName("plantTargetEnabledOnly_" + this.plantId)
let minOnly = document.getElementsByClassName("plantMinEnabledOnly_"+ this.plantId) let minOnly = document.getElementsByClassName("plantMinEnabledOnly_" + this.plantId)
console.log("updateVisibility plantConfig: " + plantConfig.mode) console.log("updateVisibility plantConfig: " + plantConfig.mode)
let showSensor = plantConfig.sensor_a || plantConfig.sensor_b let showSensor = plantConfig.sensor_a || plantConfig.sensor_b
@@ -259,7 +260,7 @@ export class PlantView {
// this.plantDiv.style.display = "none"; // this.plantDiv.style.display = "none";
// } // }
console.log("updateVisibility showsensor: " + showSensor + " pump " + showPump + " target " +showTarget + " min " + showMin) console.log("updateVisibility showsensor: " + showSensor + " pump " + showPump + " target " + showTarget + " min " + showMin)
// for (const element of Array.from(sensorOnly)) { // for (const element of Array.from(sensorOnly)) {
// if (showSensor) { // if (showSensor) {
@@ -336,7 +337,7 @@ export class PlantView {
getConfig(): PlantConfig { getConfig(): PlantConfig {
let conv: PlantConfig = { let conv: PlantConfig = {
mode: this.mode.value, mode: this.mode.value,
target_moisture: this.targetMoisture.valueAsNumber, target_moisture: this.targetMoisture.valueAsNumber,
min_moisture: this.minMoisture.valueAsNumber, min_moisture: this.minMoisture.valueAsNumber,
@@ -361,11 +362,11 @@ export class PlantView {
setDetectionResult(plantResult: DetectionPlant) { setDetectionResult(plantResult: DetectionPlant) {
console.log("setDetectionResult plantResult: " + plantResult.sensor_a + " " + plantResult.sensor_b) console.log("setDetectionResult plantResult: " + plantResult.sensor_a + " " + plantResult.sensor_b)
var changed = false; var changed = false;
if (this.sensorAInstalled.checked != plantResult.sensor_a){ if (this.sensorAInstalled.checked != plantResult.sensor_a) {
changed = true; changed = true;
this.sensorAInstalled.checked = plantResult.sensor_a; this.sensorAInstalled.checked = plantResult.sensor_a;
} }
if (this.sensorBInstalled.checked != plantResult.sensor_b){ if (this.sensorBInstalled.checked != plantResult.sensor_b) {
changed = true; changed = true;
this.sensorBInstalled.checked = plantResult.sensor_b; this.sensorBInstalled.checked = plantResult.sensor_b;
} }