Browse Source

增加设备的初始化参数下发

增加蠕动泵的使能
master
王梦远 4 days ago
parent
commit
113b6f526a
  1. 122
      src/main/java/com/iflytop/handacid/app/controller/DeviceParamController.java
  2. 27
      src/main/java/com/iflytop/handacid/app/core/command/DeviceCommandGenerator.java
  3. 62
      src/main/java/com/iflytop/handacid/app/service/DeviceInitService.java
  4. 11
      src/main/java/com/iflytop/handacid/common/model/bo/DeviceInitializationData.java
  5. 2
      src/main/java/com/iflytop/handacid/common/service/DeviceParamConfigService.java
  6. 43
      src/main/java/com/iflytop/handacid/hardware/controller/StepMotorController.java
  7. 2
      src/main/resources/application-dev.yml
  8. 14
      src/main/resources/sql/init.sql

122
src/main/java/com/iflytop/handacid/app/controller/DeviceParamController.java

@ -0,0 +1,122 @@
package com.iflytop.handacid.app.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.iflytop.handacid.common.model.entity.DeviceParamConfig;
import com.iflytop.handacid.common.model.vo.DeviceParamGroupVO;
import com.iflytop.handacid.common.model.vo.ModuleIdVO;
import com.iflytop.handacid.common.model.vo.RegIndexVO;
import com.iflytop.handacid.common.result.Result;
import com.iflytop.handacid.common.service.DeviceParamConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* 设备参数配置
*/
@Tag(name = "设备参数配置")
@RestController
@RequestMapping("/api/device-param")
@RequiredArgsConstructor
@Slf4j
public class DeviceParamController {
private final DeviceParamConfigService deviceParamConfigService;
@Operation(summary = "获取所有设备配置")
@GetMapping("/list")
public Result<List<DeviceParamGroupVO>> getList() {
List<DeviceParamGroupVO> vos = deviceParamConfigService.listGroupedByModule();
return Result.success(vos);
}
@Operation(summary = "获取所有设备模块")
@GetMapping("/modules")
public Result<List<ModuleIdVO>> listAllModules() {
List<ModuleIdVO> vos = deviceParamConfigService.listAllModuleIds();
return Result.success(vos);
}
@Operation(summary = "获取所有设备模块配置项")
@GetMapping("/reg-indices")
public Result<List<RegIndexVO>> listAllRegIndices() {
List<RegIndexVO> vos = deviceParamConfigService.listAllRegIndices();
return Result.success(vos);
}
@Operation(summary = "添加新配置")
@PostMapping("")
public Result<String> add(@Valid @RequestBody DeviceParamConfig deviceParamConfig) {
deviceParamConfigService.save(deviceParamConfig);
return Result.success();
}
@Operation(summary = "修改配置")
@PutMapping("")
public Result<String> update(@Valid @RequestBody DeviceParamConfig deviceParamConfig) {
deviceParamConfigService.updateById(deviceParamConfig);
return Result.success();
}
@Operation(summary = "删除配置")
@DeleteMapping("/{ids}")
public Result<String> delete(@Parameter(description = "矿石ID,多个以英文逗号(,)分割") @PathVariable String ids) {
boolean isSuccess = deviceParamConfigService.deleteDeviceParam(ids);
if (isSuccess) {
return Result.success();
}
return Result.failed();
}
@Operation(summary = "上传csv文件覆盖数据库配置")
@PostMapping(value = "/uploadFile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Result<String> uploadFile(@RequestPart("file") MultipartFile file) {
//文件后缀检查
if (!file.getOriginalFilename().endsWith(".csv")) {
return Result.failed("<UNK>.csv<UNK>");
}
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
String line;
deviceParamConfigService.remove(new QueryWrapper<>());//删除所有配置
//是否含有表头
boolean isHeader = true;
List<DeviceParamConfig> deviceParamConfigList = new ArrayList<>();
while ((line = reader.readLine()) != null) {
if (isHeader) {
isHeader = false;
continue; // 跳过CSV文件的标题行
}
String[] data = line.split(",");
if (data.length >= 4) { // 假设CSV文件至少包含4列
DeviceParamConfig deviceParamConfig = new DeviceParamConfig();
deviceParamConfig.setId(Long.valueOf(data[0]));
deviceParamConfig.setMid(String.valueOf(data[1]));
deviceParamConfig.setRegIndex(String.valueOf(data[2]));
deviceParamConfig.setRegVal(Integer.valueOf(data[3]));
deviceParamConfigList.add(deviceParamConfig);
deviceParamConfigService.save(deviceParamConfig);
}
}
} catch (IOException e) {
log.error("csv文件导入数据库失败:{}", e.getMessage());
throw new RuntimeException(e);
}
return Result.success();
}
}

27
src/main/java/com/iflytop/handacid/app/core/command/DeviceCommandGenerator.java

@ -12,6 +12,7 @@ public class DeviceCommandGenerator {
//=========================================== =================================================================
//=========================================== =================================================================
/**
* 1 设置速度
*/
@ -56,6 +57,13 @@ public class DeviceCommandGenerator {
}
/**
* 1 使能
*/
public static DeviceCommand pump1Enable() {
return controlCmd(Device.PUMP_1, Action.ENABLE, null);
}
/**
* 2 设置速度
*/
public static DeviceCommand pump2SetSpeed(double speed) {
@ -98,6 +106,12 @@ public class DeviceCommandGenerator {
return controlCmd(Device.PUMP_2, Action.STOP, null);
}
/**
* 2 使能
*/
public static DeviceCommand pump2Enable() {
return controlCmd(Device.PUMP_2, Action.ENABLE, null);
}
/**
* 3 设置速度
@ -142,6 +156,12 @@ public class DeviceCommandGenerator {
return controlCmd(Device.PUMP_3, Action.STOP, null);
}
/**
* 3 使能
*/
public static DeviceCommand pump3Enable() {
return controlCmd(Device.PUMP_3, Action.ENABLE, null);
}
/**
* 4 设置速度
@ -185,6 +205,13 @@ public class DeviceCommandGenerator {
public static DeviceCommand pump4Stop() {
return controlCmd(Device.PUMP_4, Action.STOP, null);
}
/**
* 4 使能
*/
public static DeviceCommand pump4Enable() {
return controlCmd(Device.PUMP_4, Action.ENABLE, null);
}
//=========================================== 私有方法 ============================================================
/**

62
src/main/java/com/iflytop/handacid/app/service/DeviceInitService.java

@ -4,14 +4,22 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.iflytop.handacid.app.common.enums.ChannelCode;
import com.iflytop.handacid.app.common.enums.SolutionAddMode;
import com.iflytop.handacid.app.common.enums.SystemConfigKey;
import com.iflytop.handacid.app.core.command.DeviceCommand;
import com.iflytop.handacid.app.core.command.DeviceCommandGenerator;
import com.iflytop.handacid.app.core.state.ChannelState;
import com.iflytop.handacid.app.core.state.DeviceState;
import com.iflytop.handacid.common.model.bo.DeviceInitializationData;
import com.iflytop.handacid.common.model.entity.Channel;
import com.iflytop.handacid.common.model.entity.DeviceParamConfig;
import com.iflytop.handacid.common.model.entity.Solution;
import com.iflytop.handacid.common.service.ChannelService;
import com.iflytop.handacid.common.service.DeviceParamConfigService;
import com.iflytop.handacid.common.service.SolutionService;
import com.iflytop.handacid.common.service.SystemConfigService;
import com.iflytop.handacid.hardware.comm.can.A8kCanBusService;
import com.iflytop.handacid.hardware.service.AppEventBusService;
import com.iflytop.handacid.hardware.type.MId;
import com.iflytop.handacid.hardware.type.RegIndex;
import com.iflytop.handacid.hardware.type.appevent.A8kCanBusOnConnectEvent;
import com.iflytop.handacid.hardware.type.appevent.AppEvent;
import jakarta.annotation.PostConstruct;
@ -20,6 +28,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
@ -29,6 +39,9 @@ public class DeviceInitService {
private final DeviceState deviceState;
private final ObjectProvider<ChannelState> channelStateObjectProvider;
private final ChannelService channelService;
private final DeviceCommandService deviceCommandService;
private final DeviceParamConfigService deviceParamConfigService;
private final A8kCanBusService canBusService;
private final SolutionService solutionService;
private final SystemConfigService systemConfigService;
@ -40,6 +53,9 @@ public class DeviceInitService {
try {
log.info("初始化开始");
initDeviceState();
initDeviceSetData();//初始化设备参数
initEnable();//使能
// initDeviceState();//初始化设备状态
log.info("初始化完毕");
} catch (Exception e) {
throw new RuntimeException(e);
@ -54,7 +70,26 @@ public class DeviceInitService {
}
}
/**
* 初始化所有设备使能
*/
public void initEnable() throws Exception {
DeviceCommand pump1Enable = DeviceCommandGenerator.pump1Enable();
deviceCommandService.sendCommand(pump1Enable);
DeviceCommand pump2Enable = DeviceCommandGenerator.pump2Enable();
deviceCommandService.sendCommand(pump2Enable);
DeviceCommand pump3Enable = DeviceCommandGenerator.pump3Enable();
deviceCommandService.sendCommand(pump3Enable);
DeviceCommand pump4Enable = DeviceCommandGenerator.pump4Enable();
deviceCommandService.sendCommand(pump4Enable);
}
/**
* 初始化设备状态
*/
public void initDeviceState() {
log.info("初始化 initDeviceState");
for (ChannelCode code : ChannelCode.values()) {
@ -69,4 +104,31 @@ public class DeviceInitService {
log.info("初始化 initDeviceState完毕");
}
/**
* 初始化设备参数
*/
public void initDeviceSetData() throws Exception {
if (deviceState.isVirtual() || deviceState.isInitComplete()) {
return;
}
List<DeviceParamConfig> deviceParamConfigs = deviceParamConfigService.list();
for (DeviceParamConfig deviceParamConfig : deviceParamConfigs) {
DeviceInitializationData data = new DeviceInitializationData();
data.setId(Math.toIntExact(deviceParamConfig.getId()));
data.setMid(deviceParamConfig.getMid());
data.setRegIndex(deviceParamConfig.getRegIndex());
data.setRegInitVal(deviceParamConfig.getRegVal());
boolean success = false; // 标记是否执行成功
while (!success) {
try {
canBusService.moduleSetReg(MId.valueOf(data.getMid()), RegIndex.valueOf(data.getRegIndex()), data.getRegInitVal());
success = true;
} catch (Exception e) {
log.error("设备初始化写入参数失败,错误: {}", e.getMessage());
Thread.sleep(2000);
}
}
}
}
}

11
src/main/java/com/iflytop/handacid/common/model/bo/DeviceInitializationData.java

@ -0,0 +1,11 @@
package com.iflytop.handacid.common.model.bo;
import lombok.Data;
@Data
public class DeviceInitializationData {
private int id;
private String mid;
private String regIndex;
private int regInitVal;
}

2
src/main/java/com/iflytop/handacid/app/service/DeviceParamConfigService.java → src/main/java/com/iflytop/handacid/common/service/DeviceParamConfigService.java

@ -1,4 +1,4 @@
package com.iflytop.handacid.app.service;
package com.iflytop.handacid.common.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;

43
src/main/java/com/iflytop/handacid/hardware/controller/StepMotorController.java

@ -1,7 +1,7 @@
package com.iflytop.handacid.hardware.controller;
import com.iflytop.handacid.app.service.DeviceParamConfigService;
import com.iflytop.handacid.common.result.Result;
import com.iflytop.handacid.common.service.DeviceParamConfigService;
import com.iflytop.handacid.hardware.exception.HardwareException;
import com.iflytop.handacid.hardware.service.StepMotorService;
import com.iflytop.handacid.hardware.type.StepMotor.DeviceStepMotorId;
@ -32,7 +32,7 @@ public class StepMotorController {
@Operation(summary = "获取设备列表")
public Map<String, String> getDeviceList() {
Map<String, String> map = new HashMap<>();
for(DeviceStepMotorId id : DeviceStepMotorId.values()) {
for (DeviceStepMotorId id : DeviceStepMotorId.values()) {
map.put(id.name(), id.getDescription());
}
return map;
@ -43,7 +43,7 @@ public class StepMotorController {
@Operation(summary = "获取寄存器列表")
public List<String> getRegList() {
List<String> list = new ArrayList<>();
for(StepMotorRegIndex reg : StepMotorRegIndex.values()) {
for (StepMotorRegIndex reg : StepMotorRegIndex.values()) {
list.add(reg.name());
}
return list;
@ -144,7 +144,7 @@ public class StepMotorController {
@RequestParam DeviceStepMotorId deviceId,
@RequestParam Integer mres) throws HardwareException {
stepMotorService.setMres(deviceId, mres);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(),StepMotorRegIndex.kreg_step_motor_mres.regIndex.name(),mres);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(), StepMotorRegIndex.kreg_step_motor_mres.regIndex.name(), mres);
return Result.success();
}
@ -154,7 +154,7 @@ public class StepMotorController {
@RequestParam DeviceStepMotorId deviceId,
@RequestParam Integer irun) throws HardwareException {
stepMotorService.setIRUN(deviceId, irun);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(),StepMotorRegIndex.kreg_step_motor_irun.regIndex.name(),irun);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(), StepMotorRegIndex.kreg_step_motor_irun.regIndex.name(), irun);
return Result.success();
}
@ -164,7 +164,7 @@ public class StepMotorController {
@RequestParam DeviceStepMotorId deviceId,
@RequestParam Integer ihold) throws HardwareException {
stepMotorService.setIHOLD(deviceId, ihold);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(),StepMotorRegIndex.kreg_step_motor_ihold.regIndex.name(),ihold);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(), StepMotorRegIndex.kreg_step_motor_ihold.regIndex.name(), ihold);
return Result.success();
}
@ -175,9 +175,9 @@ public class StepMotorController {
@RequestParam Integer v) throws HardwareException {
stepMotorService.setStartAndStopVel(deviceId, v);
//开始速度参数保存到数据库
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(),StepMotorRegIndex.kreg_step_motor_vstart.regIndex.name(),v);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(), StepMotorRegIndex.kreg_step_motor_vstart.regIndex.name(), v);
//停止速度参数保存到数据库
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(),StepMotorRegIndex.kreg_step_motor_vstop.regIndex.name(),v);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(), StepMotorRegIndex.kreg_step_motor_vstop.regIndex.name(), v);
return Result.success();
}
@ -188,7 +188,7 @@ public class StepMotorController {
@RequestParam Integer v) throws HardwareException {
stepMotorService.setV1(deviceId, v);
//参数保存到数据库
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(),StepMotorRegIndex.kreg_step_motor_v1.regIndex.name(),v);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(), StepMotorRegIndex.kreg_step_motor_v1.regIndex.name(), v);
return Result.success();
}
@ -198,8 +198,8 @@ public class StepMotorController {
@RequestParam DeviceStepMotorId deviceId,
@RequestParam Integer acc) throws HardwareException {
stepMotorService.setA1AndD1(deviceId, acc);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(),StepMotorRegIndex.kreg_step_motor_a1.regIndex.name(),acc);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(),StepMotorRegIndex.kreg_step_motor_d1.regIndex.name(),acc);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(), StepMotorRegIndex.kreg_step_motor_a1.regIndex.name(), acc);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(), StepMotorRegIndex.kreg_step_motor_d1.regIndex.name(), acc);
return Result.success();
}
@ -209,8 +209,8 @@ public class StepMotorController {
@RequestParam DeviceStepMotorId deviceId,
@RequestParam Integer acc) throws HardwareException {
stepMotorService.setAmaxAndDmax(deviceId, acc);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(),StepMotorRegIndex.kreg_step_motor_amax.regIndex.name(),acc);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(),StepMotorRegIndex.kreg_step_motor_dmax.regIndex.name(),acc);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(), StepMotorRegIndex.kreg_step_motor_amax.regIndex.name(), acc);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(), StepMotorRegIndex.kreg_step_motor_dmax.regIndex.name(), acc);
return Result.success();
}
@ -220,7 +220,7 @@ public class StepMotorController {
@RequestParam DeviceStepMotorId deviceId,
@RequestParam Integer v) throws HardwareException {
stepMotorService.setDefaultVel(deviceId, v);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(),StepMotorRegIndex.kreg_step_motor_default_velocity.regIndex.name(),v);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(), StepMotorRegIndex.kreg_step_motor_default_velocity.regIndex.name(), v);
return Result.success();
}
@ -232,9 +232,9 @@ public class StepMotorController {
@RequestParam Integer mid,
@RequestParam Integer high) throws HardwareException {
stepMotorService.setSpeed(deviceId, low, mid, high);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(),StepMotorRegIndex.kreg_step_motor_low_velocity.regIndex.name(),low);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(),StepMotorRegIndex.kreg_step_motor_mid_velocity.regIndex.name(),mid);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(),StepMotorRegIndex.kreg_step_motor_high_velocity.regIndex.name(),high);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(), StepMotorRegIndex.kreg_step_motor_low_velocity.regIndex.name(), low);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(), StepMotorRegIndex.kreg_step_motor_mid_velocity.regIndex.name(), mid);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(), StepMotorRegIndex.kreg_step_motor_high_velocity.regIndex.name(), high);
return Result.success();
}
@ -245,8 +245,8 @@ public class StepMotorController {
@RequestParam Integer pulse,
@RequestParam Integer denominator) throws HardwareException {
stepMotorService.setOneCirclePulse(deviceId, pulse, denominator);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(),StepMotorRegIndex.kreg_step_motor_one_circle_pulse.regIndex.name(),pulse);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(),StepMotorRegIndex.kreg_step_motor_one_circle_pulse_denominator.regIndex.name(),denominator);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(), StepMotorRegIndex.kreg_step_motor_one_circle_pulse.regIndex.name(), pulse);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(), StepMotorRegIndex.kreg_step_motor_one_circle_pulse_denominator.regIndex.name(), denominator);
return Result.success();
}
@ -256,7 +256,7 @@ public class StepMotorController {
@RequestParam DeviceStepMotorId deviceId,
@RequestParam Integer dzero) throws HardwareException {
stepMotorService.setDZero(deviceId, dzero);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(),StepMotorRegIndex.kreg_step_motor_dzero_pos.regIndex.name(),dzero);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(), StepMotorRegIndex.kreg_step_motor_dzero_pos.regIndex.name(), dzero);
return Result.success();
}
@ -275,7 +275,7 @@ public class StepMotorController {
@RequestParam StepMotorRegIndex reg,
@RequestParam Integer val) throws HardwareException {
stepMotorService.setReg(deviceId, reg, val);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(),reg.regIndex.name(),val);
deviceParamConfigService.setModuleAndReg(deviceId.getStepMotorMId().mid.name(), reg.regIndex.name(), val);
return Result.success();
}
@ -303,6 +303,7 @@ public class StepMotorController {
public Boolean readZeroSensor(@RequestParam DeviceStepMotorId deviceId) throws HardwareException {
return stepMotorService.readIoState(deviceId, 0);
}
@PostMapping("/read-limit-sensor")
@Operation(summary = "读取限位光电")
public Boolean readLimitSensor(@RequestParam DeviceStepMotorId deviceId) throws HardwareException {

2
src/main/resources/application-dev.yml

@ -14,7 +14,7 @@ logging:
device.enableCanBus: true
iflytophald:
ip: 127.0.0.1
ip: 192.168.73.10
cmdch.port: 19004
datach.port: 19005

14
src/main/resources/sql/init.sql

@ -114,10 +114,16 @@ INSERT OR IGNORE INTO user ( id, username, nickname, password, role, fixed_user,
(1, 'admin', 'Admin', '9973', 'ADMIN', 'ENABLE', 'DISABLE'),
(2, 'test', 'test', '9973', 'ADMIN', 'ENABLE', 'DISABLE');
-- drop table IF EXISTS zapp_sub_module_reg_initial_value;
CREATE TABLE IF NOT EXISTS zapp_sub_module_reg_initial_value (
-- 设备参数 表
CREATE TABLE IF NOT EXISTS device_param_config
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
mid TEXT,
regIndex TEXT,
regInitVal INTEGER
mid text,
reg_index text,
reg_val INTEGER,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Loading…
Cancel
Save