Browse Source

feat: odo meter 관련 로직 개발

v2
최원형 3 months ago
parent
commit
384dd7de6f
  1. 35
      src/main/java/org/mobidgim/mobidigimproject/controller/Controller.java
  2. 14
      src/main/java/org/mobidgim/mobidigimproject/model/register/RegisterDTO.java
  3. 17
      src/main/java/org/mobidgim/mobidigimproject/model/register/enums/OdoDirSource.java
  4. 18
      src/main/java/org/mobidgim/mobidigimproject/model/register/enums/OdoSpeedSource.java
  5. 20
      src/main/java/org/mobidgim/mobidigimproject/model/register/sebDTO/OdoSpeedField.java
  6. 21
      src/main/java/org/mobidgim/mobidigimproject/model/register/sebDTO/OdsDirField.java
  7. 19
      src/main/java/org/mobidgim/mobidigimproject/service/SettingService.java
  8. 42
      src/main/resources/static/css/style.css
  9. 164
      src/main/resources/static/index.html
  10. 16
      src/main/resources/static/js/api.js
  11. 28
      src/main/resources/static/js/main.js
  12. 58
      src/main/resources/static/js/navigation.js
  13. 97
      src/main/resources/static/js/protocol/can.js
  14. 102
      src/main/resources/static/js/protocol/modbus.js
  15. 66
      src/main/resources/static/js/protocol/odometer.js
  16. 93
      src/main/resources/static/js/protocol/opcua.js
  17. 111
      src/main/resources/static/js/protocol/protocolData.js
  18. 127
      src/main/resources/static/js/protocol/protocolValidation.js
  19. 1
      src/main/resources/static/js/state.js

35
src/main/java/org/mobidgim/mobidigimproject/controller/Controller.java

@ -8,6 +8,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.Map;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@ -18,6 +19,7 @@ import org.springframework.http.MediaType;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
@RestController
@ -27,13 +29,26 @@ public class Controller {
private SettingService settingService;
private Map<String, Object> buildResponse(boolean success, String message) {
Map<String, Object> response = new HashMap<>();
response.put("success", success);
response.put("message", message);
return response;
}
private Map<String, Object> buildResponse(boolean success, String message, Object data) {
Map<String, Object> response = buildResponse(success, message);
response.put("data", data);
return response;
}
@PostMapping("/device")
public ResponseEntity<Object> settingDevice(@RequestBody DeviceDTO request) throws IOException {
try{
public ResponseEntity<Map<String, Object>> settingDevice(@RequestBody DeviceDTO request) {
try {
settingService.saveFileWithDevice(request);
return ResponseEntity.ok("success");
}catch(IOException e){
return ResponseEntity.ok("save failed");
return ResponseEntity.ok(buildResponse(true, "Device settings saved successfully"));
} catch (IOException e) {
return ResponseEntity.internalServerError().body(buildResponse(false, "Device settings save failed"));
}
}
@ -46,9 +61,13 @@ public class Controller {
}
@PostMapping("/protocol")
public ResponseEntity<RegisterDTO> settingRegister(@RequestBody RegisterDTO request)throws IOException{
settingService.saveFileWithRegister(request);
return ResponseEntity.ok(request);
public ResponseEntity<Map<String, Object>> settingRegister(@RequestBody RegisterDTO request) {
try {
RegisterDTO saved = settingService.saveFileWithRegister(request);
return ResponseEntity.ok(buildResponse(true, "Protocol settings saved successfully", saved));
} catch (IOException e) {
return ResponseEntity.internalServerError().body(buildResponse(false, "Protocol settings save failed"));
}
}
@GetMapping("/get-protocol")

14
src/main/java/org/mobidgim/mobidigimproject/model/register/RegisterDTO.java

@ -8,10 +8,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.Getter;
import lombok.Setter;
import org.mobidgim.mobidigimproject.model.register.enums.*;
import org.mobidgim.mobidigimproject.model.register.sebDTO.BoolToString;
import org.mobidgim.mobidigimproject.model.register.sebDTO.CanField;
import org.mobidgim.mobidigimproject.model.register.sebDTO.ModbusField;
import org.mobidgim.mobidigimproject.model.register.sebDTO.OpcUaField;
import org.mobidgim.mobidigimproject.model.register.sebDTO.*;
import java.util.List;
@ -61,6 +58,9 @@ public class RegisterDTO {
@JsonDeserialize(using = MeidDeserializer.class)
private String meid;
@JsonProperty("odo_Meter")
private String odoMeter;
@JsonProperty("MODBUS")
private List<ModbusField> MODBUS;
@ -71,4 +71,10 @@ public class RegisterDTO {
@JsonProperty("CAN")
private List<CanField> CAN;
@JsonProperty("odo_speed")
private OdoSpeedField odometerSpeed;
@JsonProperty("odo_direction")
private OdsDirField odometerDir;
}

17
src/main/java/org/mobidgim/mobidigimproject/model/register/enums/OdoDirSource.java

@ -0,0 +1,17 @@
package org.mobidgim.mobidigimproject.model.register.enums;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public enum OdoDirSource {
CAN("can"),
HW("hw");
private final String odoDirSource;
@JsonValue
public String getOdoSpeedSource() {
return odoDirSource;
}
}

18
src/main/java/org/mobidgim/mobidigimproject/model/register/enums/OdoSpeedSource.java

@ -0,0 +1,18 @@
package org.mobidgim.mobidigimproject.model.register.enums;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public enum OdoSpeedSource {
CAN("can"),
HW("hw");
private final String odoSpeedSource;
@JsonValue
public String getOdoSpeedSource() {
return odoSpeedSource;
}
}

20
src/main/java/org/mobidgim/mobidigimproject/model/register/sebDTO/OdoSpeedField.java

@ -0,0 +1,20 @@
package org.mobidgim.mobidigimproject.model.register.sebDTO;
import lombok.Getter;
import lombok.Setter;
import org.mobidgim.mobidigimproject.model.register.enums.OdoSpeedSource;
@Getter
@Setter
public class OdoSpeedField {
private OdoSpeedSource odoSpeedSource;
private String id;
private int shift;
private String mask;
private String expr;
}

21
src/main/java/org/mobidgim/mobidigimproject/model/register/sebDTO/OdsDirField.java

@ -0,0 +1,21 @@
package org.mobidgim.mobidigimproject.model.register.sebDTO;
import lombok.Getter;
import lombok.Setter;
import org.mobidgim.mobidigimproject.model.register.enums.OdoDirSource;
import org.mobidgim.mobidigimproject.model.register.enums.OdoSpeedSource;
@Getter
@Setter
public class OdsDirField {
private OdoDirSource odoSpeedSource;
private String id;
private int shift;
private String mask;
private String expr;
}

19
src/main/java/org/mobidgim/mobidigimproject/service/SettingService.java

@ -48,6 +48,13 @@ public class SettingService {
row.setKey(KEY_DEVICE);
row.setValue(json);
boardConfigRepository.save(row); // upsert (PK=`key`)
String savedValue = boardConfigRepository.findById(KEY_DEVICE)
.map(BoardConfig::getValue)
.orElse(null);
if (!json.equals(savedValue)) {
throw new IOException("Device config verification failed after save");
}
} catch (Exception e) {
throw new IOException("Failed to save device_config to DB", e);
}
@ -90,6 +97,10 @@ public class SettingService {
if (request.getProtocol() != Protocol.OPC_UA) request.setOPC_UA(null);
if (request.getProtocol() != Protocol.MODBUS) request.setMODBUS(null);
if (request.getCanInput() != CanInput.ON) request.setCAN(null);
if (!"on".equalsIgnoreCase(request.getOdoMeter())) {
request.setOdometerSpeed(null);
request.setOdometerDir(null);
}
try {
String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(request);
@ -97,6 +108,14 @@ public class SettingService {
row.setKey(KEY_PROTOCOL);
row.setValue(json);
boardConfigRepository.save(row);
String savedValue = boardConfigRepository.findById(KEY_PROTOCOL)
.map(BoardConfig::getValue)
.orElse(null);
if (!json.equals(savedValue)) {
throw new IOException("Protocol config verification failed after save");
}
return request;
} catch (Exception e) {
throw new IOException("Failed to save protocol_config to DB", e);

42
src/main/resources/static/css/style.css

@ -287,13 +287,8 @@ details.nav {
#page-modbus th, #page-modbus td {border: 1px solid #bbb;padding: 6px 8px;text-align: left;background: #fff;}
#page-modbus th {background: #e5e5e5;font-weight: 500;}
#page-modbus tbody td {height: 34px;vertical-align: middle;}
#page-modbus .modbus-input-area {display: grid;grid-template-columns: 200px 200px 200px 200px;gap: 20px 20px;margin: 24px auto 16px auto;background: #eaeaea;padding: 16px;border-radius: 4px;margin-left: auto;margin-right: auto;}
#page-modbus .input-row {display: contents;}
#page-modbus .modbus-input-area label.nowrap {white-space: nowrap;}
#page-modbus .modbus-input-area label {min-width: auto;color: #444;white-space: normal;}
#page-modbus .modbus-input-area input {background: #e0e0e0;border: none;border-radius: 2px;padding: 4px 8px;min-width: auto;}
#page-modbus .modbus-actions {display: flex;gap: 24px;justify-content: center;margin: 16px 0 0 0;}
#page-modbus .modbus-actions button {background: #d3d3d3;border: 1px solid #aaa;border-radius: 2px;padding: 8px 24px;font-size: 16px;cursor: pointer;}
#page-modbus td input,
#page-modbus td select {width: 100%;border: 1px solid #c7c7c7;border-radius: 2px;padding: 6px 8px;background: #fdfdfd;font: inherit;box-sizing: border-box;}
/* 하단 버튼 영역 */
#modbus-bottom-actions {display: flex;gap: 24px;justify-content: center;background: #f7f7f7;border: 1px solid #bbb;border-radius: 4px;margin: 24px 0 0 0;padding: 20px 0;}
#modbus-bottom-actions button {background: #e0e0e0;border: 1px solid #aaa;border-radius: 2px;padding: 10px 18px;font-size: 16px;min-width: 140px;cursor: pointer;}
@ -304,13 +299,8 @@ details.nav {
#page-opcua th, #page-opcua td {border: 1px solid #bbb;padding: 6px 8px;text-align: left;background: #fff;}
#page-opcua th {background: #e5e5e5;font-weight: 500;}
#page-opcua tbody td {height: 34px;vertical-align: middle;}
#page-opcua .opcua-input-area {display: grid;grid-template-columns: 200px 200px 200px 200px;gap: 20px 20px;margin: 24px auto 16px auto;background: #eaeaea;padding: 16px;border-radius: 4px;margin-left: auto;margin-right: auto;}
#page-opcua .input-row {display: contents;}
#page-opcua .opcua-input-area label.nowrap {white-space: nowrap;}
#page-opcua .opcua-input-area label {min-width: auto;color: #444;white-space: normal;}
#page-opcua .opcua-input-area input {background: #e0e0e0;border: none;border-radius: 2px;padding: 4px 8px;min-width: auto;}
#page-opcua .opcua-actions {display: flex;gap: 24px;justify-content: center;margin: 16px 0 0 0;}
#page-opcua .opcua-actions button {background: #d3d3d3;border: 1px solid #aaa;border-radius: 2px;padding: 8px 24px;font-size: 16px;cursor: pointer;}
#page-opcua td input,
#page-opcua td select {width: 100%;border: 1px solid #c7c7c7;border-radius: 2px;padding: 6px 8px;background: #fdfdfd;font: inherit;box-sizing: border-box;}
#opcUa-bottom-actions {display: flex;gap: 24px;justify-content: center;background: #f7f7f7;border: 1px solid #bbb;border-radius: 4px;margin: 24px 0 0 0;padding: 20px 0;}
#opcUa-bottom-actions button {background: #e0e0e0;border: 1px solid #aaa;border-radius: 2px;padding: 10px 18px;font-size: 16px;min-width: 140px;cursor: pointer;}
@ -321,16 +311,26 @@ details.nav {
#page-can th {background: #e5e5e5;font-weight: 500;}
#page-can tbody td {height: 34px;vertical-align: middle;}
#page-can tbody td > span:empty::before {content: "\00a0";}
#page-can .can-input-area {display: grid;grid-template-columns: 200px 200px 200px 200px;gap: 20px 20px;margin: 24px auto 16px auto;background: #eaeaea;padding: 16px;border-radius: 4px;margin-left: auto;margin-right: auto;}
#page-can .input-row {display: contents;}
#page-can .can-input-area label.nowrap {white-space: nowrap;}
#page-can .can-input-area label {min-width: auto;color: #444;white-space: normal;}
#page-can .can-input-area input {background: #e0e0e0;border: none;border-radius: 2px;padding: 4px 8px;min-width: auto;}
#page-can .can-actions {display: flex;gap: 24px;justify-content: center;margin: 16px 0 0 0;}
#page-can .can-actions button {background: #d3d3d3;border: 1px solid #aaa;border-radius: 2px;padding: 8px 24px;font-size: 16px;cursor: pointer;}
#page-can td input,
#page-can td select {width: 100%;border: 1px solid #c7c7c7;border-radius: 2px;padding: 6px 8px;background: #fdfdfd;font: inherit;box-sizing: border-box;}
#can-bottom-actions {display: flex;gap: 24px;justify-content: center;background: #f7f7f7;border: 1px solid #bbb;border-radius: 4px;margin: 24px 0 0 0;padding: 20px 0;}
#can-bottom-actions button {background: #e0e0e0;border: 1px solid #aaa;border-radius: 2px;padding: 10px 18px;font-size: 16px;min-width: 140px;cursor: pointer;}
.inline-table-actions {display: flex;justify-content: flex-end;margin-top: 16px;}
.inline-table-actions button,
.inline-delete-btn {background: #e0e0e0;border: 1px solid #aaa;border-radius: 2px;padding: 8px 16px;font-size: 14px;cursor: pointer;}
/* Odometer page styles */
#page-odometer .card {background: #f7f7f7;border: 1px solid #bbb;box-shadow: none;padding: 24px;border-radius: 4px;display: flex;flex-direction: column;}
#page-odometer table {width: 100%;border-collapse: collapse;background: #fff;border: 1px solid #bbb;}
#page-odometer th, #page-odometer td {border: 1px solid #bbb;padding: 6px 8px;text-align: left;background: #fff;}
#page-odometer th {background: #e5e5e5;font-weight: 500;}
#page-odometer tbody td {height: 34px;vertical-align: middle;}
#page-odometer td input,
#page-odometer td select {width: 100%;padding: 4px 6px;border: 1px solid #aaa;border-radius: 4px;font-size: 14px;background: #fff;box-sizing: border-box;}
#odometer-bottom-actions {display: flex;gap: 24px;justify-content: center;background: #f7f7f7;border: 1px solid #bbb;border-radius: 4px;margin: 24px 0 0 0;padding: 20px 0;}
#odometer-bottom-actions button {background: #e0e0e0;border: 1px solid #aaa;border-radius: 2px;padding: 10px 18px;font-size: 16px;min-width: 140px;cursor: pointer;}
.selected-row {background: #d0e6ff !important;}
.selected-row > td {background: #d0e6ff !important;}

164
src/main/resources/static/index.html

@ -26,6 +26,7 @@
<button data-page="modbus" id="modbusSidebarBtn" style="display: none">MODBUS</button>
<button data-page="opcua" id="opcUaSidebarBtn" style="display: none">OPC_UA</button>
<button data-page="can" id="canSidebarBtn" style="display: none">CAN</button>
<button data-page="odometer" id="odometerSidebarBtn" style="display: none">ODOMETER</button>
</div>
</details>
</aside>
@ -380,17 +381,6 @@
</div>
</section>
<template id="modbus-row-template">
<tr>
<td><span class="field-display"></span></td>
<td><span class="addr-display"></span></td>
<td><span class="idt-display"></span></td>
<td><span class="odt-display"></span></td>
<td><span class="dv-display"></span></td>
<td><span class="expr-display"></span></td>
</tr>
</template>
<section id="page-modbus" class="page" style="display:none;">
<div class="card">
<h3>Modbus Configuration</h3>
@ -404,46 +394,15 @@
<th>ODT</th>
<th>Default Value</th>
<th>Expression</th>
<th>Action</th>
</tr>
</thead>
<tbody id="modbus-field-list">
</tbody>
</table>
</div>
<div class="modbus-input-area">
<label>Field</label>
<input type="text" id="modbus_field_input" placeholder="Enter field name">
<label class="nowrap">idt (Input Data Type)</label>
<select id="modbus_idt_input">
<option selected disabled hidden="hidden"></option>
<option value="integer">integer</option>
<option value="float">float</option>
<option value="boolean">boolean</option>
<option value="unsignedInteger">unsignedInteger</option>
<option value="short">short</option>
<option value="unsignedShort">unsignedShort</option>
</select>
<label>addr</label>
<input type="text" id="modbus_addr_input" placeholder="Enter address">
<label class="nowrap">odt (Output Data Type)</label>
<select id="modbus_odt_input">
<option selected disabled hidden="hidden"></option>
<option value="integer">integer</option>
<option value="float64">float64</option>
<option value="boolean">boolean</option>
<option value="string">string</option>
</select>
<label class="nowrap">dv</label>
<input type="text" id="modbus_dv_input" placeholder="Enter default value">
<label>expr</label>
<input type="text" id="modbus_expr_input" placeholder="Enter expression">
</div>
<div class="modbus-actions">
<button id="modbus_add_btn">Add</button>
<button id="modbus_modify_btn">Modify</button>
<button id="modbus_delete_btn">Delete</button>
<div class="inline-table-actions">
<button id="modbus_add_row_btn" type="button">Add Row</button>
</div>
</div>
@ -461,17 +420,6 @@
</section>
<template id="opcua-row-template">
<tr class="opcua-row">
<td><span class="opcua-field-display"></span></td>
<td><span class="opcua-addr-display"></span></td>
<td><span class="opcua-ns-display"></span></td>
<td><span class="opcua-odt-display"></span></td>
<td><span class="opcua-dv-display"></span></td>
<td><span class="opcua-expr-display"></span></td>
</tr>
</template>
<section id="page-opcua" class="page" style="display:none;">
<div class="card">
<h3>OPC_UA Configuration</h3>
@ -485,38 +433,15 @@
<th>ODT</th>
<th>Default Value</th>
<th>Expression</th>
<th>Action</th>
</tr>
</thead>
<tbody id="opcua-field-list">
</tbody>
</table>
</div>
<div class="opcua-input-area">
<label>field</label>
<input type="text" id="opcua_field_input" placeholder="Enter field name">
<label class="nowrap">ns(NameSpace)</label>
<input type="text" id="opcua_ns_input" placeholder="Enter NS">
<label>addr(address)</label>
<input type="text" id="opcua_addr_input" placeholder="Enter address">
<label class="nowrap">odt(Output Data Type)</label>
<select id="opcua_odt_input">
<option selected disabled hidden="hidden"></option>
<option value="integer">integer</option>
<option value="float64">float64</option>
<option value="boolean">boolean</option>
<option value="string">string</option>
</select>
<label class="nowrap">dv(Default Value)</label>
<input type="text" id="opcua_dv_input" placeholder="Enter default value">
<label>expr</label>
<input type="text" id="opcua_expr_input" placeholder="Enter expression">
</div>
<div class="opcua-actions">
<button id="opcua_add_btn">Add</button>
<button id="opcua_modify_btn">Modify</button>
<button id="opcua_delete_btn">Delete</button>
<div class="inline-table-actions">
<button id="opcua_add_row_btn" type="button">Add Row</button>
</div>
</div>
@ -534,18 +459,6 @@
</section>
<template id="can-row-template">
<tr class="can-row">
<td><span class="can-field-display"></span></td>
<td><span class="can-id-display"></span></td>
<td><span class="can-odt-display"></span></td>
<td><span class="can-dv-display"></span></td>
<td><span class="can-shift-display"></span></td>
<td><span class="can-expr-display"></span></td>
<td><span class="can-mask-display"></span></td>
</tr>
</template>
<section id="page-can" class="page" style="display:none;">
<div class="card">
<h3>CAN Configuration</h3>
@ -560,38 +473,15 @@
<th>Shift</th>
<th>Expression</th>
<th>Mask</th>
<th>Action</th>
</tr>
</thead>
<tbody id="can-field-list">
</tbody>
</table>
</div>
<div class="can-input-area">
<label>Field</label>
<input type="text" id="can_field_input" placeholder="Enter field name">
<label>ID</label>
<input type="text" id="can_id_input" placeholder="Enter ID">
<label>odt (Output Data Type)</label>
<select id="can_odt_input">
<option selected disabled hidden="hidden"></option>
<option value="integer">integer</option>
<option value="float64">float64</option>
<option value="boolean">boolean</option>
<option value="string">string</option>
</select>
<label>dv(Default Value)</label>
<input type="text" id="can_dv_input" placeholder="Enter default value">
<label>shift</label>
<input type="text" id="can_shift_input" placeholder="Enter shift value">
<label>expr</label>
<input type="text" id="can_expr_input" placeholder="Enter expression">
<label>mask</label>
<input type="text" id="can_mask_input" placeholder="Enter mask">
</div>
<div class="can-actions">
<button id="can_add_btn">Add</button>
<button id="can_modify_btn">Modify</button>
<button id="can_delete_btn">Delete</button>
<div class="inline-table-actions">
<button id="can_add_row_btn" type="button">Add Row</button>
</div>
</div>
<div id="can-bottom-actions">
@ -608,6 +498,40 @@
</section>
<section id="page-odometer" class="page" style="display:none;">
<div class="card">
<h3>Odometer Configuration</h3>
<div class="odometer-table-container">
<table>
<thead>
<tr>
<th>Target</th>
<th>Source</th>
<th>ID</th>
<th>Shift</th>
<th>Expression</th>
<th>Mask</th>
</tr>
</thead>
<tbody id="odometer-field-list">
</tbody>
</table>
</div>
</div>
<div id="odometer-bottom-actions">
<div class="file-upload-wrapper-protocol"
data-file-type="protocol"
data-expected-filename="protocolConfig.json"
data-upload-url="/setting/protocol"
data-get-url="/setting/get-protocol">
<input type="file" class="upload-input-protocol" accept=".json" style="display: none;">
<button class="select-file-btn-protocol">Load Local Protocol File</button>
</div>
<button class="download-protocol-file">Download Protocol File</button>
</div>
</section>
</main>

16
src/main/resources/static/js/api.js

@ -19,7 +19,21 @@ async function postJson(url, payload) {
body: JSON.stringify(payload),
});
return response;
const contentType = response.headers.get('content-type') || '';
let data = null;
if (contentType.includes('application/json')) {
data = await response.json();
} else {
const text = await response.text();
data = text ? { message: text } : null;
}
return {
ok: response.ok,
status: response.status,
data,
};
}
export function loadDeviceConfig() {

28
src/main/resources/static/js/main.js

@ -5,9 +5,10 @@ import { bindDeviceFileActions, bindProtocolFileActions } from './device/deviceF
import { createSsidController } from './device/ssid.js';
import { createCanController } from './protocol/can.js';
import { createModbusController } from './protocol/modbus.js';
import { createOdometerController } from './protocol/odometer.js';
import { createOpcUaController } from './protocol/opcua.js';
import { createProtocolDataController } from './protocol/protocolData.js';
import { isValidRegisterConfig } from './protocol/protocolValidation.js';
import { getRegisterConfigValidationError, isValidRegisterConfig } from './protocol/protocolValidation.js';
import { byId, queryAll } from './utils/dom.js';
async function initializeApp() {
@ -25,6 +26,7 @@ async function initializeApp() {
modbusController: createModbusController(),
opcUaController: createOpcUaController(),
canController: createCanController(),
odometerController: createOdometerController(),
});
navigation.initialize();
@ -39,8 +41,9 @@ async function initializeApp() {
bindProtocolFileActions({
onLoadLocal: (data) => {
if (!isValidRegisterConfig(data)) {
alert('The contents of the selected file are not in a valid protocol setting file format.');
const validationError = getRegisterConfigValidationError(data);
if (validationError) {
alert(`The contents of the selected file are not valid.\n${validationError}`);
return;
}
@ -56,8 +59,9 @@ async function initializeApp() {
protocolController.collectProtocolData(),
);
if (!isValidRegisterConfig(protocolPayload)) {
alert('Register settings are currently invalid. Cannot be saved.');
const validationError = getRegisterConfigValidationError(protocolPayload);
if (validationError) {
alert(`Register settings are currently invalid.\n${validationError}`);
return;
}
@ -67,13 +71,21 @@ async function initializeApp() {
saveProtocolConfig(protocolPayload),
]);
if (deviceResponse.ok && protocolResponse.ok) {
const isDeviceSaved = deviceResponse.ok && deviceResponse.data?.success === true;
const isProtocolSaved = protocolResponse.ok && protocolResponse.data?.success === true;
if (isDeviceSaved && isProtocolSaved) {
alert('saved successfully');
return;
}
alert('save failure');
console.error('device response:', deviceResponse.status, 'protocol response:', protocolResponse.status);
const failureMessages = [
!isDeviceSaved ? (deviceResponse.data?.message || `Device save failed (${deviceResponse.status})`) : null,
!isProtocolSaved ? (protocolResponse.data?.message || `Protocol save failed (${protocolResponse.status})`) : null,
].filter(Boolean);
alert(failureMessages.join('\n') || 'save failure');
console.error('device response:', deviceResponse, 'protocol response:', protocolResponse);
} catch (error) {
console.error('Error during transfer:', error);
alert('save failure');

58
src/main/resources/static/js/navigation.js

@ -10,10 +10,14 @@ export function createNavigation() {
modbus: byId('page-modbus'),
opcua: byId('page-opcua'),
can: byId('page-can'),
odometer: byId('page-odometer'),
};
const titleElement = byId('page-title');
const sidebarButtons = queryAll('.nav-items button');
const protocolSelect = byId('reg_protocol');
const canInputSelect = byId('reg_can_input');
const odometerSelect = byId('OdoMeter');
function showPage(pageName) {
Object.values(pages).forEach((page) => {
@ -30,27 +34,61 @@ export function createNavigation() {
titleElement.textContent = sourceButton?.textContent ?? pageName;
}
function toggleProtocolMenus(protocolValue, canInputValue) {
function toggleProtocolMenus(protocolValue, canInputValue, odometerValue) {
const modbusSidebarBtn = byId('modbusSidebarBtn');
const opcUaSidebarBtn = byId('opcUaSidebarBtn');
const canSidebarBtn = byId('canSidebarBtn');
const odometerSidebarBtn = byId('odometerSidebarBtn');
const normalizedProtocol = String(protocolValue ?? '').trim().toUpperCase();
const normalizedCanInput = String(canInputValue ?? '').trim().toLowerCase();
const normalizedOdometer = String(odometerValue ?? '').trim().toLowerCase();
const visibleProtocolPages = new Set();
if (modbusSidebarBtn) {
modbusSidebarBtn.style.display = protocolValue === 'MODBUS' ? 'block' : 'none';
const isVisible = normalizedProtocol === 'MODBUS';
modbusSidebarBtn.style.display = isVisible ? 'block' : 'none';
if (isVisible) {
visibleProtocolPages.add('modbus');
}
}
if (opcUaSidebarBtn) {
opcUaSidebarBtn.style.display = protocolValue === 'OPC_UA' ? 'block' : 'none';
const isVisible = normalizedProtocol === 'OPC_UA';
opcUaSidebarBtn.style.display = isVisible ? 'block' : 'none';
if (isVisible) {
visibleProtocolPages.add('opcua');
}
}
if (canSidebarBtn) {
canSidebarBtn.style.display = canInputValue === 'on' ? 'block' : 'none';
const isVisible = normalizedCanInput === 'on';
canSidebarBtn.style.display = isVisible ? 'block' : 'none';
if (isVisible) {
visibleProtocolPages.add('can');
}
}
if (odometerSidebarBtn) {
const isVisible = normalizedOdometer === 'on';
odometerSidebarBtn.style.display = isVisible ? 'block' : 'none';
if (isVisible) {
visibleProtocolPages.add('odometer');
}
}
const protocolDetails = queryAll('details.nav')[1];
if (protocolDetails) {
protocolDetails.open = true;
}
const hiddenActivePage = ['modbus', 'opcua', 'can', 'odometer'].find((pageName) => {
const page = pages[pageName];
return page && page.style.display !== 'none' && !visibleProtocolPages.has(pageName);
});
if (hiddenActivePage) {
showPage('reg');
}
}
function initialize() {
@ -58,7 +96,19 @@ export function createNavigation() {
button.addEventListener('click', () => showPage(button.dataset.page));
});
const syncFromInputs = () => toggleProtocolMenus(
protocolSelect?.value,
canInputSelect?.value,
odometerSelect?.value,
);
[protocolSelect, canInputSelect, odometerSelect].forEach((element) => {
element?.addEventListener('change', syncFromInputs);
element?.addEventListener('input', syncFromInputs);
});
showPage('ssid');
syncFromInputs();
}
return {

97
src/main/resources/static/js/protocol/can.js

@ -1,30 +1,73 @@
import { byId, createTableEditor } from '../utils/dom.js';
import { byId } from '../utils/dom.js';
function buildRow(data = {}) {
return `
<tr class="can-row">
<td><input class="can-field-input" value="${data.field ?? ''}"></td>
<td><input class="can-id-input" value="${data.id ?? ''}"></td>
<td>
<select class="can-odt-input">
<option value="integer" ${data.odt === 'integer' ? 'selected' : ''}>integer</option>
<option value="float64" ${data.odt === 'float64' ? 'selected' : ''}>float64</option>
<option value="boolean" ${data.odt === 'boolean' ? 'selected' : ''}>boolean</option>
<option value="string" ${data.odt === 'string' ? 'selected' : ''}>string</option>
</select>
</td>
<td><input class="can-dv-input" value="${data.dv ?? ''}"></td>
<td><input class="can-shift-input" value="${data.shift ?? ''}"></td>
<td><input class="can-expr-input" value="${data.expr ?? ''}"></td>
<td><input class="can-mask-input" value="${data.mask ?? ''}"></td>
<td><button type="button" class="inline-delete-btn">Delete</button></td>
</tr>
`;
}
export function createCanController() {
return createTableEditor({
listElement: byId('can-field-list'),
templateElement: byId('can-row-template'),
selectors: {
field: '.can-field-display',
id: '.can-id-display',
odt: '.can-odt-display',
dv: '.can-dv-display',
shift: '.can-shift-display',
expr: '.can-expr-display',
mask: '.can-mask-display',
},
inputIds: {
field: 'can_field_input',
id: 'can_id_input',
odt: 'can_odt_input',
dv: 'can_dv_input',
shift: 'can_shift_input',
expr: 'can_expr_input',
mask: 'can_mask_input',
},
addButtonId: 'can_add_btn',
modifyButtonId: 'can_modify_btn',
deleteButtonId: 'can_delete_btn',
emptyInputSelector: '.can-input-area input, .can-input-area select',
});
const listElement = byId('can-field-list');
const addButton = byId('can_add_row_btn');
function renderRows(rows = []) {
const sourceRows = rows.length ? rows : [{}];
listElement.innerHTML = sourceRows.map((row) => buildRow(row)).join('');
}
function serializeRows(requiredKey) {
return Array.from(listElement.querySelectorAll('tr'))
.map((row) => ({
field: row.querySelector('.can-field-input')?.value?.trim() ?? '',
id: row.querySelector('.can-id-input')?.value?.trim() ?? '',
odt: row.querySelector('.can-odt-input')?.value?.trim() ?? '',
dv: row.querySelector('.can-dv-input')?.value?.trim() ?? '',
shift: row.querySelector('.can-shift-input')?.value?.trim() ?? '',
expr: row.querySelector('.can-expr-input')?.value?.trim() ?? '',
mask: row.querySelector('.can-mask-input')?.value?.trim() ?? '',
}))
.filter((row) => row[requiredKey]);
}
function addRow() {
listElement.insertAdjacentHTML('beforeend', buildRow());
}
function initialize() {
renderRows();
addButton?.addEventListener('click', addRow);
listElement.addEventListener('click', (event) => {
if (!event.target.classList.contains('inline-delete-btn')) {
return;
}
event.target.closest('tr')?.remove();
if (!listElement.children.length) {
renderRows();
}
});
}
return {
initialize,
renderRows,
serializeRows,
};
}

102
src/main/resources/static/js/protocol/modbus.js

@ -1,28 +1,80 @@
import { byId, createTableEditor } from '../utils/dom.js';
import { byId } from '../utils/dom.js';
function buildRow(data = {}) {
return `
<tr class="modbus-row">
<td><input class="modbus-field-input" value="${data.field ?? ''}"></td>
<td><input class="modbus-addr-input" value="${data.addr ?? ''}"></td>
<td>
<select class="modbus-idt-input">
<option value="integer" ${data.idt === 'integer' ? 'selected' : ''}>integer</option>
<option value="float" ${data.idt === 'float' ? 'selected' : ''}>float</option>
<option value="boolean" ${data.idt === 'boolean' ? 'selected' : ''}>boolean</option>
<option value="unsignedInteger" ${data.idt === 'unsignedInteger' ? 'selected' : ''}>unsignedInteger</option>
<option value="short" ${data.idt === 'short' ? 'selected' : ''}>short</option>
<option value="unsignedShort" ${data.idt === 'unsignedShort' ? 'selected' : ''}>unsignedShort</option>
</select>
</td>
<td>
<select class="modbus-odt-input">
<option value="integer" ${data.odt === 'integer' ? 'selected' : ''}>integer</option>
<option value="float64" ${data.odt === 'float64' ? 'selected' : ''}>float64</option>
<option value="boolean" ${data.odt === 'boolean' ? 'selected' : ''}>boolean</option>
<option value="string" ${data.odt === 'string' ? 'selected' : ''}>string</option>
</select>
</td>
<td><input class="modbus-dv-input" value="${data.dv ?? ''}"></td>
<td><input class="modbus-expr-input" value="${data.expr ?? ''}"></td>
<td><button type="button" class="inline-delete-btn">Delete</button></td>
</tr>
`;
}
export function createModbusController() {
return createTableEditor({
listElement: byId('modbus-field-list'),
templateElement: byId('modbus-row-template'),
selectors: {
field: '.field-display',
addr: '.addr-display',
idt: '.idt-display',
odt: '.odt-display',
dv: '.dv-display',
expr: '.expr-display',
},
inputIds: {
field: 'modbus_field_input',
addr: 'modbus_addr_input',
idt: 'modbus_idt_input',
odt: 'modbus_odt_input',
dv: 'modbus_dv_input',
expr: 'modbus_expr_input',
},
addButtonId: 'modbus_add_btn',
modifyButtonId: 'modbus_modify_btn',
deleteButtonId: 'modbus_delete_btn',
emptyInputSelector: '.modbus-input-area input, .modbus-input-area select',
});
const listElement = byId('modbus-field-list');
const addButton = byId('modbus_add_row_btn');
function renderRows(rows = []) {
const sourceRows = rows.length ? rows : [{}];
listElement.innerHTML = sourceRows.map((row) => buildRow(row)).join('');
}
function serializeRows(requiredKey) {
return Array.from(listElement.querySelectorAll('tr'))
.map((row) => ({
field: row.querySelector('.modbus-field-input')?.value?.trim() ?? '',
addr: row.querySelector('.modbus-addr-input')?.value?.trim() ?? '',
idt: row.querySelector('.modbus-idt-input')?.value?.trim() ?? '',
odt: row.querySelector('.modbus-odt-input')?.value?.trim() ?? '',
dv: row.querySelector('.modbus-dv-input')?.value?.trim() ?? '',
expr: row.querySelector('.modbus-expr-input')?.value?.trim() ?? '',
}))
.filter((row) => row[requiredKey]);
}
function addRow() {
listElement.insertAdjacentHTML('beforeend', buildRow());
}
function initialize() {
renderRows();
addButton?.addEventListener('click', addRow);
listElement.addEventListener('click', (event) => {
if (!event.target.classList.contains('inline-delete-btn')) {
return;
}
event.target.closest('tr')?.remove();
if (!listElement.children.length) {
renderRows();
}
});
}
return {
initialize,
renderRows,
serializeRows,
};
}

66
src/main/resources/static/js/protocol/odometer.js

@ -0,0 +1,66 @@
import { byId } from '../utils/dom.js';
function buildRow(target, data = {}) {
const safeData = data && typeof data === 'object' ? data : {};
return `
<tr class="odometer-row" data-target="${target}">
<td>${target}</td>
<td>
<select class="odometer-source-input">
<option value="can" ${safeData.odo_speed_source === 'can' ? 'selected' : ''}>can</option>
<option value="hw" ${safeData.odo_speed_source === 'hw' ? 'selected' : ''}>hw</option>
</select>
</td>
<td><input class="odometer-id-input" value="${safeData.id ?? ''}"></td>
<td><input class="odometer-shift-input" value="${safeData.shift ?? ''}"></td>
<td><input class="odometer-expr-input" value="${safeData.expr ?? ''}"></td>
<td><input class="odometer-mask-input" value="${safeData.mask ?? ''}"></td>
</tr>
`;
}
export function createOdometerController() {
const listElement = byId('odometer-field-list');
function renderRows(data = {}) {
const safeData = data && typeof data === 'object' ? data : {};
listElement.innerHTML = [
buildRow('speed', safeData.odo_speed),
buildRow('direction', safeData.odo_direction),
].join('');
}
function readRow(target) {
const row = listElement.querySelector(`tr[data-target="${target}"]`);
if (!row) {
return null;
}
return {
odo_speed_source: row.querySelector('.odometer-source-input')?.value?.trim() ?? '',
id: row.querySelector('.odometer-id-input')?.value?.trim() ?? '',
shift: row.querySelector('.odometer-shift-input')?.value?.trim() ?? '',
expr: row.querySelector('.odometer-expr-input')?.value?.trim() ?? '',
mask: row.querySelector('.odometer-mask-input')?.value?.trim() ?? '',
};
}
function collectData() {
return {
odo_speed: readRow('speed'),
odo_direction: readRow('direction'),
};
}
function initialize() {
renderRows();
}
return {
collectData,
initialize,
renderRows,
};
}

93
src/main/resources/static/js/protocol/opcua.js

@ -1,28 +1,71 @@
import { byId, createTableEditor } from '../utils/dom.js';
import { byId } from '../utils/dom.js';
function buildRow(data = {}) {
return `
<tr class="opcua-row">
<td><input class="opcua-field-input" value="${data.field ?? ''}"></td>
<td><input class="opcua-addr-input" value="${data.addr ?? ''}"></td>
<td><input class="opcua-ns-input" value="${data.ns ?? ''}"></td>
<td>
<select class="opcua-odt-input">
<option value="integer" ${data.odt === 'integer' ? 'selected' : ''}>integer</option>
<option value="float64" ${data.odt === 'float64' ? 'selected' : ''}>float64</option>
<option value="boolean" ${data.odt === 'boolean' ? 'selected' : ''}>boolean</option>
<option value="string" ${data.odt === 'string' ? 'selected' : ''}>string</option>
</select>
</td>
<td><input class="opcua-dv-input" value="${data.dv ?? ''}"></td>
<td><input class="opcua-expr-input" value="${data.expr ?? ''}"></td>
<td><button type="button" class="inline-delete-btn">Delete</button></td>
</tr>
`;
}
export function createOpcUaController() {
return createTableEditor({
listElement: byId('opcua-field-list'),
templateElement: byId('opcua-row-template'),
selectors: {
field: '.opcua-field-display',
addr: '.opcua-addr-display',
ns: '.opcua-ns-display',
odt: '.opcua-odt-display',
dv: '.opcua-dv-display',
expr: '.opcua-expr-display',
},
inputIds: {
field: 'opcua_field_input',
addr: 'opcua_addr_input',
ns: 'opcua_ns_input',
odt: 'opcua_odt_input',
dv: 'opcua_dv_input',
expr: 'opcua_expr_input',
},
addButtonId: 'opcua_add_btn',
modifyButtonId: 'opcua_modify_btn',
deleteButtonId: 'opcua_delete_btn',
emptyInputSelector: '.opcua-input-area input, .opcua-input-area select',
});
const listElement = byId('opcua-field-list');
const addButton = byId('opcua_add_row_btn');
function renderRows(rows = []) {
const sourceRows = rows.length ? rows : [{}];
listElement.innerHTML = sourceRows.map((row) => buildRow(row)).join('');
}
function serializeRows(requiredKey) {
return Array.from(listElement.querySelectorAll('tr'))
.map((row) => ({
field: row.querySelector('.opcua-field-input')?.value?.trim() ?? '',
addr: row.querySelector('.opcua-addr-input')?.value?.trim() ?? '',
ns: row.querySelector('.opcua-ns-input')?.value?.trim() ?? '',
odt: row.querySelector('.opcua-odt-input')?.value?.trim() ?? '',
dv: row.querySelector('.opcua-dv-input')?.value?.trim() ?? '',
expr: row.querySelector('.opcua-expr-input')?.value?.trim() ?? '',
}))
.filter((row) => row[requiredKey]);
}
function addRow() {
listElement.insertAdjacentHTML('beforeend', buildRow());
}
function initialize() {
renderRows();
addButton?.addEventListener('click', addRow);
listElement.addEventListener('click', (event) => {
if (!event.target.classList.contains('inline-delete-btn')) {
return;
}
event.target.closest('tr')?.remove();
if (!listElement.children.length) {
renderRows();
}
});
}
return {
initialize,
renderRows,
serializeRows,
};
}

111
src/main/resources/static/js/protocol/protocolData.js

@ -5,6 +5,45 @@ export const normProtocol = (value) => String(value ?? '').trim().replace(/[-\s]
export const getOpcArr = (data) => data?.OPC_UA ?? data?.['OPC-UA'] ?? [];
const checkboxIds = ['ai0', 'ai1', 'ai2', 'ai3', 'di0', 'di1', 'di2', 'di3'];
const toLower = (value) => String(value ?? '').trim().toLowerCase();
const toUpper = (value) => String(value ?? '').trim().toUpperCase();
function normalizeFourByteValue(value) {
const normalized = toLower(value).replace(/[_\-\s]+/g, '');
switch (normalized) {
case 'little':
return 'little';
case 'littleswap':
return 'littleSwap';
case 'bigswap':
return 'bigSwap';
case 'big':
return 'big';
default:
return String(value ?? '').trim();
}
}
function normalizeValueForElement(htmlId, value) {
switch (htmlId) {
case 'reg_protocol':
return normProtocol(value);
case 'reg_can_input':
case 'reg_dr_on_off':
case 'OdoMeter':
case 'reg_two_byte_order':
return toLower(value);
case 'reg_four_byte_order':
return normalizeFourByteValue(value);
case 'reg_device_type':
case 'reg_equipment':
case 'reg_speed_data_source':
return toUpper(value);
default:
return value ?? '';
}
}
export function normalizeRegisterForPost(register) {
const normalized = JSON.parse(JSON.stringify(register || {}));
@ -18,6 +57,14 @@ export function normalizeRegisterForPost(register) {
normalized.can_input = String(normalized.can_input).trim().toUpperCase();
}
if (normalized.OdoMeter) {
normalized.OdoMeter = String(normalized.OdoMeter).trim().toLowerCase();
}
if (normalized.odo_Meter) {
normalized.odo_Meter = String(normalized.odo_Meter).trim().toLowerCase();
}
if (normalized.MEID !== undefined && normalized.MEID !== null) {
const meid = String(normalized.MEID).trim();
normalized.MEID = /^\d+$/.test(meid) ? Number(meid) : meid;
@ -43,17 +90,50 @@ export function normalizeRegisterForPost(register) {
return normalized;
}
export function createProtocolDataController({ navigation, modbusController, opcUaController, canController }) {
export function createProtocolDataController({
navigation,
modbusController,
opcUaController,
canController,
odometerController,
}) {
const protocolSelect = byId('reg_protocol');
const canInputSelect = byId('reg_can_input');
const drOnOffSelect = byId('reg_dr_on_off');
const odometerSelect = byId('OdoMeter');
function syncProtocolMenus() {
navigation.toggleProtocolMenus(protocolSelect?.value, canInputSelect?.value);
navigation.toggleProtocolMenus(protocolSelect?.value, canInputSelect?.value, odometerSelect?.value);
}
function syncProtocolMenusDeferred() {
syncProtocolMenus();
window.requestAnimationFrame(syncProtocolMenus);
window.setTimeout(syncProtocolMenus, 0);
window.setTimeout(syncProtocolMenus, 50);
}
function syncOdometerAvailability() {
if (!odometerSelect) {
return;
}
const isDrEnabled = toLower(drOnOffSelect?.value) === 'on';
odometerSelect.disabled = !isDrEnabled;
if (!isDrEnabled) {
odometerSelect.value = 'off';
}
}
function bindEvents() {
drOnOffSelect?.addEventListener('change', () => {
syncOdometerAvailability();
syncProtocolMenus();
});
protocolSelect?.addEventListener('change', syncProtocolMenus);
canInputSelect?.addEventListener('change', syncProtocolMenus);
odometerSelect?.addEventListener('change', syncProtocolMenus);
}
function collectProtocolData() {
@ -77,6 +157,20 @@ export function createProtocolDataController({ navigation, modbusController, opc
register.OPC_UA = opcUaController.serializeRows('field');
register.CAN = canController.serializeRows('field');
const odometerData = odometerController.collectData();
if (toLower(register.dr_on) !== 'on') {
register.odo_Meter = 'off';
register.odo_speed = null;
register.odo_direction = null;
} else if (String(register.odo_Meter ?? '').toLowerCase() === 'on') {
register.odo_speed = odometerData.odo_speed;
register.odo_direction = odometerData.odo_direction;
} else {
register.odo_speed = null;
register.odo_direction = null;
}
return register;
}
@ -98,7 +192,7 @@ export function createProtocolDataController({ navigation, modbusController, opc
if (element.type === 'checkbox') {
element.checked = value === '1' || value === true;
} else {
element.value = value ?? '';
element.value = normalizeValueForElement(htmlId, value);
}
return;
}
@ -126,15 +220,22 @@ export function createProtocolDataController({ navigation, modbusController, opc
modbusController.renderRows(Array.isArray(register.MODBUS) ? register.MODBUS : []);
opcUaController.renderRows(Array.isArray(getOpcArr(register)) ? getOpcArr(register) : []);
canController.renderRows(Array.isArray(register.CAN) ? register.CAN : []);
syncProtocolMenus();
odometerController.renderRows({
odo_speed: register.odo_speed ?? null,
odo_direction: register.odo_direction ?? null,
});
syncOdometerAvailability();
syncProtocolMenusDeferred();
}
function initialize() {
modbusController.initialize();
opcUaController.initialize();
canController.initialize();
odometerController.initialize();
bindEvents();
syncProtocolMenus();
syncOdometerAvailability();
syncProtocolMenusDeferred();
}
return {

127
src/main/resources/static/js/protocol/protocolValidation.js

@ -1,103 +1,106 @@
import { getOpcArr, normProtocol } from './protocolData.js';
export function isValidRegisterConfig(data) {
if (typeof data !== 'object' || data === null) {
console.error('Validation Error: Data is not an object or is null.');
function hasAnyValue(item, keys) {
if (!item || typeof item !== 'object') {
return false;
}
return keys.some((key) => String(item[key] ?? '').trim() !== '');
}
export function getRegisterConfigValidationError(data) {
if (typeof data !== 'object' || data === null) {
return 'Data is not an object or is null.';
}
if (typeof data.protocol === 'string') {
data.protocol = normProtocol(data.protocol);
}
const requiredFields = [
'protocol',
'analog_input_level',
'can_input',
'dev_type',
'dr_on',
'equipment',
'two_byte_order',
'four_byte_order',
'speed_data',
'version',
'equipment_id',
'MEID',
];
for (const field of requiredFields) {
if (!Object.prototype.hasOwnProperty.call(data, field)) {
console.error(`Validation Error: Missing required top-level field: '${field}'`);
return false;
}
const validProtocols = ['MODBUS', 'OPC_UA', 'CAN', 'NONE'];
if (data.protocol !== undefined && data.protocol !== '' && (typeof data.protocol !== 'string' || !validProtocols.includes(data.protocol))) {
return `Invalid protocol '${data.protocol}'.`;
}
const validProtocols = ['MODBUS', 'OPC_UA', 'CAN', 'NONE'];
if (typeof data.protocol !== 'string' || !validProtocols.includes(data.protocol)) {
console.error(`Validation Error: Invalid protocol '${data.protocol}'`);
return false;
if (data.analog_input_level !== undefined
&& data.analog_input_level !== null
&& typeof data.analog_input_level !== 'string'
&& typeof data.analog_input_level !== 'number') {
return "'analog_input_level' is not a string or number.";
}
if (typeof data.analog_input_level !== 'string' && typeof data.analog_input_level !== 'number') {
console.error("Validation Error: 'analog_input_level' is not a string or number.");
return false;
if (String(data.dr_on ?? '').toLowerCase() !== 'on' && String(data.odo_Meter ?? '').toLowerCase() === 'on') {
return "'odo_Meter' cannot be on when 'dr_on' is off.";
}
if (data.protocol === 'MODBUS') {
if (!Array.isArray(data.MODBUS)) {
console.error("Validation Error: 'MODBUS' must be an array when protocol is MODBUS.");
return false;
}
for (const [index, item] of (Array.isArray(data.MODBUS) ? data.MODBUS : []).entries()) {
if (!hasAnyValue(item, ['field', 'idt', 'addr', 'odt', 'dv', 'expr'])) {
continue;
}
for (const [index, item] of data.MODBUS.entries()) {
for (const key of ['field', 'idt', 'addr', 'odt', 'dv']) {
if (!item || String(item[key] ?? '').trim() === '') {
console.error(`Validation Error: MODBUS row ${index} missing '${key}'.`);
return false;
if (String(item?.[key] ?? '').trim() === '') {
return `MODBUS row ${index + 1} missing '${key}'.`;
}
}
}
} else if (Array.isArray(data.MODBUS) && data.MODBUS.length > 0) {
console.error("Validation Error: 'MODBUS' must be empty when protocol is not MODBUS.");
return false;
}
if (data.protocol === 'OPC_UA') {
const opcRows = getOpcArr(data);
if (!Array.isArray(opcRows)) {
console.error("Validation Error: 'OPC_UA' must be an array when protocol is OPC_UA.");
return false;
}
for (const [index, item] of getOpcArr(data).entries()) {
if (!hasAnyValue(item, ['field', 'ns', 'addr', 'odt', 'dv', 'expr'])) {
continue;
}
for (const [index, item] of opcRows.entries()) {
for (const key of ['field', 'ns', 'addr', 'odt', 'dv']) {
if (!item || String(item[key] ?? '').trim() === '') {
console.error(`Validation Error: OPC_UA row ${index} missing '${key}'.`);
return false;
if (String(item?.[key] ?? '').trim() === '') {
return `OPC_UA row ${index + 1} missing '${key}'.`;
}
}
}
} else if (data.OPC_UA && !Array.isArray(data.OPC_UA)) {
console.error("Validation Error: 'OPC_UA' must be an array if present.");
return false;
return "'OPC_UA' must be an array if present.";
}
if (String(data.can_input).toLowerCase() === 'on') {
if (!Array.isArray(data.CAN)) {
console.error("Validation Error: 'CAN' is required when can_input is on.");
return false;
}
for (const [index, item] of (Array.isArray(data.CAN) ? data.CAN : []).entries()) {
if (!hasAnyValue(item, ['field', 'id', 'odt', 'dv', 'shift', 'expr', 'mask'])) {
continue;
}
for (const [index, item] of data.CAN.entries()) {
for (const key of ['field', 'id', 'odt', 'dv']) {
if (!item || String(item[key] ?? '').trim() === '') {
console.error(`Validation Error: CAN row ${index} missing '${key}'.`);
return false;
if (String(item?.[key] ?? '').trim() === '') {
return `CAN row ${index + 1} missing '${key}'.`;
}
}
}
}
if (String(data.odo_Meter ?? '').toLowerCase() === 'on') {
const speed = data.odo_speed;
const direction = data.odo_direction;
for (const [label, item] of [['odo_speed', speed], ['odo_direction', direction]]) {
if (!hasAnyValue(item, ['odo_speed_source', 'id', 'shift', 'expr', 'mask'])) {
continue;
}
for (const key of ['odo_speed_source', 'id', 'shift']) {
if (String(item[key] ?? '').trim() === '') {
return `${label} missing '${key}'.`;
}
}
}
} else if (Array.isArray(data.CAN) && data.CAN.length > 0) {
console.error("Validation Error: 'CAN' must be empty when can_input is off.");
}
return null;
}
export function isValidRegisterConfig(data) {
const error = getRegisterConfigValidationError(data);
if (error) {
console.error(`Validation Error: ${error}`);
return false;
}

1
src/main/resources/static/js/state.js

@ -55,6 +55,7 @@ export const REGISTER_ID_MAP = {
reg_version: 'version',
reg_equipment_id: 'equipment_id',
reg_meid: 'MEID',
OdoMeter: 'odo_Meter',
};
const appState = {

Loading…
Cancel
Save