ProjectConfigManager.java 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. package com.mes.core;
  2. import com.alibaba.fastjson2.JSON;
  3. import com.alibaba.fastjson2.JSONObject;
  4. import com.alibaba.fastjson2.JSONWriter;
  5. import org.slf4j.Logger;
  6. import org.slf4j.LoggerFactory;
  7. import java.io.IOException;
  8. import java.nio.charset.StandardCharsets;
  9. import java.nio.file.Files;
  10. import java.nio.file.Path;
  11. import java.nio.file.Paths;
  12. import java.util.ArrayList;
  13. import java.util.Arrays;
  14. import java.util.Collections;
  15. import java.util.LinkedHashMap;
  16. import java.util.List;
  17. import java.util.Map;
  18. /**
  19. * 项目配置管理器
  20. * 负责切换"产线位置"和"产品号"(两者互相独立、可自由组合)
  21. * 以及拉铆设备IP的运行时持久化
  22. *
  23. * 产线位置(Line):决定工位号→工序名称的映射表(配合 OprnoRegistry 使用)
  24. * 产品号(Product):决定工件码/冷板码/底护板码前缀
  25. *
  26. * 两张表内置,避免误改;运行时可变的仅"当前产线位置id"、"当前产品号id"和"设备IP"
  27. */
  28. public class ProjectConfigManager {
  29. private static final Logger log = LoggerFactory.getLogger(ProjectConfigManager.class);
  30. private static final String CONFIG_DIR = "config";
  31. private static final String CONFIG_FILE = "project_config.json";
  32. // ========== 产线位置定义 ==========
  33. public static final class Line {
  34. private final String id; // P1/P2/P3/P4
  35. public Line(String id) {
  36. this.id = id;
  37. }
  38. public String getId() { return id; }
  39. /** 供UI下拉显示 */
  40. public String getDisplay() { return id; }
  41. }
  42. // ========== 产品号定义 ==========
  43. public static final class Product {
  44. private final String id; // P02-1/AY7-610/AY7-520/P04-1/650
  45. private final String productPrefix; // 工件码(框架码) 前缀
  46. private final String coldPlatePrefix; // 冷板码 前缀
  47. private final String bottomPlatePrefix; // 底护板码 前缀
  48. public Product(String id, String productPrefix, String coldPlatePrefix, String bottomPlatePrefix) {
  49. this.id = id;
  50. this.productPrefix = productPrefix;
  51. this.coldPlatePrefix = coldPlatePrefix;
  52. this.bottomPlatePrefix = bottomPlatePrefix;
  53. }
  54. public String getId() { return id; }
  55. public String getProductPrefix() { return productPrefix; }
  56. public String getColdPlatePrefix() { return coldPlatePrefix; }
  57. public String getBottomPlatePrefix() { return bottomPlatePrefix; }
  58. /** 供UI下拉显示 */
  59. public String getDisplay() { return id; }
  60. }
  61. // 内置产线位置列表(顺序即 UI 展示顺序)
  62. private static final List<Line> LINES = Collections.unmodifiableList(Arrays.asList(
  63. new Line("P1"),
  64. new Line("P2"),
  65. new Line("P3"),
  66. new Line("P4")
  67. ));
  68. // 内置产品号列表(顺序即 UI 展示顺序)
  69. private static final List<Product> PRODUCTS = Collections.unmodifiableList(Arrays.asList(
  70. new Product("P02-1", "+KA99", "+KB004", "+KB77"),
  71. new Product("AY7-610", "+KB24", "+KA94", "+KB77"),
  72. new Product("AY7-520", "+KB23", "+KB004", "+KB77"),
  73. new Product("P04-1", "+KA93", "+KA94", "+KB77"),
  74. new Product("650", "+KA64IS", "+KA64IG", "+KB78")
  75. ));
  76. // 默认值
  77. private static final String DEFAULT_LINE_ID = "P2";
  78. private static final String DEFAULT_PRODUCT_ID = "AY7-610";
  79. private static final String DEFAULT_DEVICE_IP = "192.168.0.6";
  80. // 旧配置迁移:老版本 currentProjectId(P1~P4) → 产品号 的对应关系
  81. private static final Map<String, String> LEGACY_PROJECT_TO_PRODUCT;
  82. static {
  83. Map<String, String> m = new LinkedHashMap<>();
  84. m.put("P1", "P02-1");
  85. m.put("P2", "AY7-610");
  86. m.put("P3", "AY7-520");
  87. m.put("P4", "P04-1");
  88. LEGACY_PROJECT_TO_PRODUCT = Collections.unmodifiableMap(m);
  89. }
  90. // ========== 单例 ==========
  91. private static volatile ProjectConfigManager instance;
  92. public static ProjectConfigManager getInstance() {
  93. if (instance == null) {
  94. synchronized (ProjectConfigManager.class) {
  95. if (instance == null) {
  96. instance = new ProjectConfigManager();
  97. }
  98. }
  99. }
  100. return instance;
  101. }
  102. // ========== 状态 ==========
  103. private final Path configPath;
  104. private String currentLineId;
  105. private String currentProductId;
  106. private String deviceIp;
  107. private String stationCodeOverride; // 覆盖 yaml 的 stations[0].code
  108. private String lineSnOverride; // 覆盖 yaml 的 line_sn
  109. private String serverIpOverride; // 覆盖 yaml 的 server.ip
  110. private final Map<String, Boolean> featuresOverride = new LinkedHashMap<>();
  111. // 设备连接覆盖(null=不覆盖,走yaml默认)
  112. private Boolean deviceEnabledOverride;
  113. // 设备信息行覆盖:是否启用 + label(寄存器地址不在UI改)
  114. private Boolean deviceRow1EnabledOverride;
  115. private String deviceRow1LabelOverride;
  116. private Boolean deviceRow2EnabledOverride;
  117. private String deviceRow2LabelOverride;
  118. // 物料码输入框显隐覆盖
  119. private Boolean showMaterialInputOverride;
  120. // 拉铆过程参数上传类型 (A/B/C/D),默认 A
  121. private String prodParamsType = "A";
  122. private final List<Listener> listeners = new ArrayList<>();
  123. private ProjectConfigManager() {
  124. this.configPath = Paths.get(CONFIG_DIR, CONFIG_FILE);
  125. this.currentLineId = DEFAULT_LINE_ID;
  126. this.currentProductId = DEFAULT_PRODUCT_ID;
  127. this.deviceIp = DEFAULT_DEVICE_IP;
  128. load();
  129. }
  130. // ========== 加载/保存 ==========
  131. private void load() {
  132. if (!Files.exists(configPath)) {
  133. log.info("[ProjectConfig] 配置文件不存在,使用默认: line={}, product={}, ip={}", currentLineId, currentProductId, deviceIp);
  134. return;
  135. }
  136. try {
  137. String content = new String(Files.readAllBytes(configPath), StandardCharsets.UTF_8);
  138. JSONObject json = JSON.parseObject(content);
  139. if (json == null) return;
  140. // 新字段:产线位置 + 产品号
  141. String lineId = json.getString("currentLineId");
  142. if (lineId != null && findLine(lineId) != null) {
  143. currentLineId = lineId;
  144. }
  145. String productId = json.getString("currentProductId");
  146. if (productId != null && findProduct(productId) != null) {
  147. currentProductId = productId;
  148. }
  149. // 兼容旧配置:老版本只有 currentProjectId(如"P4",旧版项目id同时代表产线位置+产品号)
  150. // 若新字段缺失,则按旧的 id→产品号 对应关系拆分迁移
  151. if (lineId == null || productId == null) {
  152. String oldPid = json.getString("currentProjectId");
  153. if (oldPid != null) {
  154. if (lineId == null && findLine(oldPid) != null) {
  155. currentLineId = oldPid;
  156. }
  157. if (productId == null) {
  158. String legacyProduct = LEGACY_PROJECT_TO_PRODUCT.get(oldPid.toUpperCase());
  159. if (legacyProduct != null) {
  160. currentProductId = legacyProduct;
  161. }
  162. }
  163. }
  164. }
  165. String ip = json.getString("deviceIp");
  166. if (ip != null && !ip.trim().isEmpty()) {
  167. deviceIp = ip.trim();
  168. }
  169. // 加载功能开关覆盖
  170. JSONObject featuresObj = json.getJSONObject("featuresOverride");
  171. if (featuresObj != null) {
  172. featuresOverride.clear();
  173. for (String key : featuresObj.keySet()) {
  174. Boolean v = featuresObj.getBoolean(key);
  175. if (v != null) featuresOverride.put(key, v);
  176. }
  177. }
  178. String sc = json.getString("stationCodeOverride");
  179. if (sc != null && !sc.trim().isEmpty()) {
  180. stationCodeOverride = sc.trim();
  181. }
  182. String ls = json.getString("lineSnOverride");
  183. if (ls != null && !ls.trim().isEmpty()) {
  184. lineSnOverride = ls.trim();
  185. }
  186. String sIp = json.getString("serverIpOverride");
  187. if (sIp != null && !sIp.trim().isEmpty()) {
  188. serverIpOverride = sIp.trim();
  189. }
  190. // 设备连接覆盖
  191. if (json.containsKey("deviceEnabledOverride")) {
  192. deviceEnabledOverride = json.getBoolean("deviceEnabledOverride");
  193. }
  194. // 设备信息行
  195. JSONObject uiObj = json.getJSONObject("uiOverride");
  196. if (uiObj != null) {
  197. deviceRow1EnabledOverride = uiObj.getBoolean("deviceRow1Enabled");
  198. deviceRow1LabelOverride = trimToNull(uiObj.getString("deviceRow1Label"));
  199. deviceRow2EnabledOverride = uiObj.getBoolean("deviceRow2Enabled");
  200. deviceRow2LabelOverride = trimToNull(uiObj.getString("deviceRow2Label"));
  201. showMaterialInputOverride = uiObj.getBoolean("showMaterialInput");
  202. String pt = trimToNull(uiObj.getString("prodParamsType"));
  203. if (pt != null) prodParamsType = pt.toUpperCase();
  204. }
  205. log.info("[ProjectConfig] 加载成功: line={}, product={}, ip={}, station={}, lineSn={}, features={}",
  206. currentLineId, currentProductId, deviceIp, stationCodeOverride, lineSnOverride, featuresOverride);
  207. } catch (Exception e) {
  208. log.error("[ProjectConfig] 加载失败: {}", e.getMessage(), e);
  209. }
  210. }
  211. private void save() {
  212. try {
  213. if (configPath.getParent() != null) {
  214. Files.createDirectories(configPath.getParent());
  215. }
  216. JSONObject root = new JSONObject();
  217. root.put("currentLineId", currentLineId);
  218. root.put("currentProductId", currentProductId);
  219. root.put("deviceIp", deviceIp);
  220. if (stationCodeOverride != null) root.put("stationCodeOverride", stationCodeOverride);
  221. if (lineSnOverride != null) root.put("lineSnOverride", lineSnOverride);
  222. if (serverIpOverride != null) root.put("serverIpOverride", serverIpOverride);
  223. if (deviceEnabledOverride != null) root.put("deviceEnabledOverride", deviceEnabledOverride);
  224. JSONObject uiObj = new JSONObject();
  225. if (deviceRow1EnabledOverride != null) uiObj.put("deviceRow1Enabled", deviceRow1EnabledOverride);
  226. if (deviceRow1LabelOverride != null) uiObj.put("deviceRow1Label", deviceRow1LabelOverride);
  227. if (deviceRow2EnabledOverride != null) uiObj.put("deviceRow2Enabled", deviceRow2EnabledOverride);
  228. if (deviceRow2LabelOverride != null) uiObj.put("deviceRow2Label", deviceRow2LabelOverride);
  229. if (showMaterialInputOverride != null) uiObj.put("showMaterialInput", showMaterialInputOverride);
  230. uiObj.put("prodParamsType", prodParamsType);
  231. if (!uiObj.isEmpty()) root.put("uiOverride", uiObj);
  232. if (!featuresOverride.isEmpty()) {
  233. root.put("featuresOverride", featuresOverride);
  234. }
  235. root.put("lastModified", System.currentTimeMillis());
  236. String content = JSON.toJSONString(root, JSONWriter.Feature.PrettyFormat);
  237. Files.write(configPath, content.getBytes(StandardCharsets.UTF_8));
  238. log.info("[ProjectConfig] 保存成功: {}", configPath);
  239. } catch (IOException e) {
  240. log.error("[ProjectConfig] 保存失败: {}", e.getMessage(), e);
  241. }
  242. }
  243. // ========== 产线位置查询 ==========
  244. public List<Line> getLines() {
  245. return LINES;
  246. }
  247. public Line findLine(String id) {
  248. if (id == null) return null;
  249. for (Line l : LINES) {
  250. if (l.getId().equalsIgnoreCase(id)) return l;
  251. }
  252. return null;
  253. }
  254. public Line getCurrentLine() {
  255. Line l = findLine(currentLineId);
  256. return l != null ? l : LINES.get(0);
  257. }
  258. public String getCurrentLineId() {
  259. return currentLineId;
  260. }
  261. // ========== 产品号查询 ==========
  262. public List<Product> getProducts() {
  263. return PRODUCTS;
  264. }
  265. public Product findProduct(String id) {
  266. if (id == null) return null;
  267. for (Product p : PRODUCTS) {
  268. if (p.getId().equalsIgnoreCase(id)) return p;
  269. }
  270. return null;
  271. }
  272. public Product getCurrentProduct() {
  273. Product p = findProduct(currentProductId);
  274. return p != null ? p : PRODUCTS.get(0);
  275. }
  276. public String getCurrentProductId() {
  277. return currentProductId;
  278. }
  279. // ========== 前缀快捷方法 ==========
  280. /** 工件码(框架码)前缀,若当前产品找不到则返回null */
  281. public String getProductPrefix() {
  282. Product p = getCurrentProduct();
  283. return p != null ? p.getProductPrefix() : null;
  284. }
  285. /** 冷板码前缀 */
  286. public String getColdPlatePrefix() {
  287. Product p = getCurrentProduct();
  288. return p != null ? p.getColdPlatePrefix() : null;
  289. }
  290. /** 底护板码前缀 */
  291. public String getBottomPlatePrefix() {
  292. Product p = getCurrentProduct();
  293. return p != null ? p.getBottomPlatePrefix() : null;
  294. }
  295. /**
  296. * 根据物料标签获取前缀(供 ScanMaterialStep 区分)
  297. * 支持"冷板码""底护板码"两种标签
  298. */
  299. public String getMaterialPrefixByLabel(String label) {
  300. if (label == null) return null;
  301. if (label.contains("冷板")) return getColdPlatePrefix();
  302. if (label.contains("底护板")) return getBottomPlatePrefix();
  303. return null;
  304. }
  305. // ========== 切换产线位置 ==========
  306. public void switchLine(String lineId) {
  307. Line l = findLine(lineId);
  308. if (l == null) {
  309. log.warn("[ProjectConfig] 无效的产线位置id: {}", lineId);
  310. return;
  311. }
  312. if (l.getId().equalsIgnoreCase(currentLineId)) return;
  313. String old = currentLineId;
  314. currentLineId = l.getId();
  315. save();
  316. log.info("[ProjectConfig] 切换产线位置: {} -> {}", old, currentLineId);
  317. fireLineChanged(l);
  318. }
  319. // ========== 切换产品号 ==========
  320. public void switchProduct(String productId) {
  321. Product p = findProduct(productId);
  322. if (p == null) {
  323. log.warn("[ProjectConfig] 无效的产品号id: {}", productId);
  324. return;
  325. }
  326. if (p.getId().equalsIgnoreCase(currentProductId)) return;
  327. String old = currentProductId;
  328. currentProductId = p.getId();
  329. save();
  330. log.info("[ProjectConfig] 切换产品号: {} -> {}", old, currentProductId);
  331. fireProductChanged(p);
  332. }
  333. // ========== 设备IP ==========
  334. public String getDeviceIp() {
  335. return deviceIp;
  336. }
  337. public void setDeviceIp(String ip) {
  338. if (ip == null || ip.trim().isEmpty()) return;
  339. ip = ip.trim();
  340. if (ip.equals(this.deviceIp)) return;
  341. String old = this.deviceIp;
  342. this.deviceIp = ip;
  343. save();
  344. log.info("[ProjectConfig] 修改拉铆设备IP: {} -> {}", old, ip);
  345. fireDeviceIpChanged(ip);
  346. }
  347. // ========== 功能开关覆盖 ==========
  348. /**
  349. * 获取功能开关覆盖值,null 表示未覆盖(走 yaml 默认)
  350. */
  351. public Boolean getFeatureOverride(String feature) {
  352. return featuresOverride.get(feature);
  353. }
  354. public Map<String, Boolean> getFeaturesOverride() {
  355. return new LinkedHashMap<>(featuresOverride);
  356. }
  357. /**
  358. * 批量设置功能开关覆盖(UI一次提交多项时用)
  359. * newValues 为 null 或空则清空覆盖(恢复 yaml 默认)
  360. */
  361. public void setFeaturesOverride(Map<String, Boolean> newValues) {
  362. featuresOverride.clear();
  363. if (newValues != null) {
  364. for (Map.Entry<String, Boolean> e : newValues.entrySet()) {
  365. if (e.getKey() != null && e.getValue() != null) {
  366. featuresOverride.put(e.getKey(), e.getValue());
  367. }
  368. }
  369. }
  370. save();
  371. log.info("[ProjectConfig] 更新功能开关: {}", featuresOverride);
  372. fireFeaturesChanged();
  373. }
  374. // ========== 工位号/线体覆盖 ==========
  375. public String getStationCodeOverride() { return stationCodeOverride; }
  376. public String getLineSnOverride() { return lineSnOverride; }
  377. public String getServerIpOverride() { return serverIpOverride; }
  378. /**
  379. * 设置服务器(MES)IP 覆盖
  380. */
  381. public void setServerIpOverride(String ip) {
  382. if (ip == null) return;
  383. ip = ip.trim();
  384. if (ip.isEmpty()) return;
  385. if (ip.equalsIgnoreCase(this.serverIpOverride)) return;
  386. this.serverIpOverride = ip;
  387. save();
  388. log.info("[ProjectConfig] 修改服务器IP: {}", ip);
  389. fireServerIpChanged(ip);
  390. }
  391. public Boolean getDeviceRow1EnabledOverride() { return deviceRow1EnabledOverride; }
  392. /**
  393. * 拉铆过程参数上传类型 (A/B/C/D),默认 A
  394. */
  395. public String getProdParamsType() {
  396. return prodParamsType != null ? prodParamsType : "A";
  397. }
  398. /**
  399. * 设置拉铆过程参数上传类型
  400. * @param type 有效值 A/B/C/D(大小写不敏感,其它值忽略)
  401. */
  402. public void setProdParamsType(String type) {
  403. if (type == null) return;
  404. String t = type.trim().toUpperCase();
  405. if (t.isEmpty()) return;
  406. if (!"A".equals(t) && !"B".equals(t) && !"C".equals(t) && !"D".equals(t)) {
  407. log.warn("[ProjectConfig] 无效的拉铆参数类型: {}, 忽略", type);
  408. return;
  409. }
  410. if (t.equals(this.prodParamsType)) return;
  411. String old = this.prodParamsType;
  412. this.prodParamsType = t;
  413. save();
  414. log.info("[ProjectConfig] 修改拉铆参数类型: {} -> {}", old, t);
  415. }
  416. // ========== 设备连接覆盖 ==========
  417. public Boolean getDeviceEnabledOverride() { return deviceEnabledOverride; }
  418. public void setDeviceEnabledOverride(Boolean enabled) {
  419. this.deviceEnabledOverride = enabled;
  420. save();
  421. log.info("[ProjectConfig] 设备连接覆盖: {}", enabled);
  422. }
  423. public String getDeviceRow1LabelOverride() { return deviceRow1LabelOverride; }
  424. public Boolean getDeviceRow2EnabledOverride() { return deviceRow2EnabledOverride; }
  425. public String getDeviceRow2LabelOverride() { return deviceRow2LabelOverride; }
  426. public Boolean getShowMaterialInputOverride() { return showMaterialInputOverride; }
  427. /**
  428. * 更新设备信息行UI覆盖(启用/label),null 表示不覆盖
  429. */
  430. public void setDeviceInfoRowsOverride(Boolean row1Enabled, String row1Label,
  431. Boolean row2Enabled, String row2Label,
  432. Boolean showMaterialInput) {
  433. this.deviceRow1EnabledOverride = row1Enabled;
  434. this.deviceRow1LabelOverride = trimToNull(row1Label);
  435. this.deviceRow2EnabledOverride = row2Enabled;
  436. this.deviceRow2LabelOverride = trimToNull(row2Label);
  437. this.showMaterialInputOverride = showMaterialInput;
  438. save();
  439. log.info("[ProjectConfig] UI覆盖: row1=({},{}), row2=({},{}), showMaterial={}",
  440. row1Enabled, deviceRow1LabelOverride,
  441. row2Enabled, deviceRow2LabelOverride,
  442. showMaterialInput);
  443. fireUiChanged();
  444. }
  445. private static String trimToNull(String s) {
  446. if (s == null) return null;
  447. String t = s.trim();
  448. return t.isEmpty() ? null : t;
  449. }
  450. /**
  451. * 设置工位号与线体覆盖(任一可为null表示不覆盖该项)
  452. */
  453. public void setStationOverride(String stationCode, String lineSn) {
  454. boolean changed = false;
  455. if (stationCode != null) {
  456. stationCode = stationCode.trim();
  457. if (!stationCode.isEmpty() && !stationCode.equalsIgnoreCase(this.stationCodeOverride)) {
  458. this.stationCodeOverride = stationCode;
  459. changed = true;
  460. }
  461. }
  462. if (lineSn != null) {
  463. lineSn = lineSn.trim();
  464. if (!lineSn.isEmpty() && !lineSn.equalsIgnoreCase(this.lineSnOverride)) {
  465. this.lineSnOverride = lineSn;
  466. changed = true;
  467. }
  468. }
  469. if (changed) {
  470. save();
  471. log.info("[ProjectConfig] 修改工位: code={}, lineSn={}", stationCodeOverride, lineSnOverride);
  472. fireStationChanged();
  473. }
  474. }
  475. // ========== 事件监听(UI订阅) ==========
  476. public interface Listener {
  477. default void onLineChanged(Line line) {}
  478. default void onProductChanged(Product product) {}
  479. default void onDeviceIpChanged(String newIp) {}
  480. default void onServerIpChanged(String newIp) {}
  481. default void onFeaturesChanged() {}
  482. default void onStationChanged() {}
  483. default void onUiChanged() {}
  484. }
  485. public void addListener(Listener l) {
  486. if (l != null && !listeners.contains(l)) listeners.add(l);
  487. }
  488. public void removeListener(Listener l) {
  489. listeners.remove(l);
  490. }
  491. private void fireLineChanged(Line l) {
  492. for (Listener listener : new ArrayList<>(listeners)) {
  493. try { listener.onLineChanged(l); } catch (Exception e) { log.warn("listener error", e); }
  494. }
  495. }
  496. private void fireProductChanged(Product p) {
  497. for (Listener listener : new ArrayList<>(listeners)) {
  498. try { listener.onProductChanged(p); } catch (Exception e) { log.warn("listener error", e); }
  499. }
  500. }
  501. private void fireDeviceIpChanged(String ip) {
  502. for (Listener l : new ArrayList<>(listeners)) {
  503. try { l.onDeviceIpChanged(ip); } catch (Exception e) { log.warn("listener error", e); }
  504. }
  505. }
  506. private void fireServerIpChanged(String ip) {
  507. for (Listener l : new ArrayList<>(listeners)) {
  508. try { l.onServerIpChanged(ip); } catch (Exception e) { log.warn("listener error", e); }
  509. }
  510. }
  511. private void fireFeaturesChanged() {
  512. for (Listener l : new ArrayList<>(listeners)) {
  513. try { l.onFeaturesChanged(); } catch (Exception e) { log.warn("listener error", e); }
  514. }
  515. }
  516. private void fireStationChanged() {
  517. for (Listener l : new ArrayList<>(listeners)) {
  518. try { l.onStationChanged(); } catch (Exception e) { log.warn("listener error", e); }
  519. }
  520. }
  521. private void fireUiChanged() {
  522. for (Listener l : new ArrayList<>(listeners)) {
  523. try { l.onUiChanged(); } catch (Exception e) { log.warn("listener error", e); }
  524. }
  525. }
  526. }