weiyigulu 3 年之前
當前提交
07656c0e59

+ 106 - 0
pom.xml

@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.example</groupId>
+    <artifactId>104SLAVER</artifactId>
+    <version>1.0-SNAPSHOT</version>
+
+
+
+    <properties>
+        <maven.compiler.source>8</maven.compiler.source>
+        <maven.compiler.target>8</maven.compiler.target>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>wei.yigulu</groupId>
+            <artifactId>protocol-iec104</artifactId>
+            <version>1.4.25</version>
+        </dependency>
+        <dependency>
+            <groupId>com.googlecode.aviator</groupId>
+            <artifactId>aviator</artifactId>
+            <version>5.0.0</version>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>1.8</source>
+                    <target>1.8</target>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <version>2.4.3</version>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                        <configuration>
+                            <createDependencyReducedPom>false</createDependencyReducedPom>
+                            <transformers>
+                                <transformer
+                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                                    <mainClass>StartHttpServer</mainClass>
+                                </transformer>
+                            </transformers>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>2.18.1</version>
+                <configuration>
+                    <skipTests>true</skipTests>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.sonatype.plugins</groupId>
+                <artifactId>nexus-staging-maven-plugin</artifactId>
+                <version>1.5.1</version>
+                <executions>
+                    <execution>
+                        <id>default-deploy</id>
+                        <phase>deploy</phase>
+                        <goals>
+                            <goal>deploy</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <serverId>ias-releases</serverId>
+                    <nexusUrl>https://maven.mangoautomation.net/</nexusUrl>
+                    <skipStaging>true</skipStaging>
+                </configuration>
+            </plugin>
+            <plugin>
+                <artifactId>maven-source-plugin</artifactId>
+                <version>2.1</version>
+                <configuration>
+                    <attach>true</attach>
+                </configuration>
+                <executions>
+                    <execution>
+                        <phase>compile</phase>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+        </plugins>
+    </build>
+</project>

+ 25 - 0
src/main/java/BasePointData.java

@@ -0,0 +1,25 @@
+import lombok.Getter;
+
+public abstract class BasePointData {
+
+    protected ProtocolDataContainer protocolDataContainer=ProtocolDataContainer.getInstance();
+
+    /**
+     * 点位地址
+     */
+    @Getter
+    private Integer point;
+
+    @Getter
+    private String value;
+
+
+    public BasePointData(Integer point,String value){
+        this.point=point;
+        this.value=value;
+    }
+
+
+    public abstract  void joinRealTimeData();
+
+}

+ 34 - 0
src/main/java/GetStaticFileUtils.java

@@ -0,0 +1,34 @@
+import io.netty.util.internal.StringUtil;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+public class GetStaticFileUtils {
+    public static String getFileContext(String fileName){
+        StringBuilder sb =new StringBuilder();
+        InputStream resourceAsStream = GetStaticFileUtils.class.getResourceAsStream(fileName);
+        if(resourceAsStream==null){
+            return sb.toString();
+        }
+        try {
+            BufferedReader br = new BufferedReader(
+                    new InputStreamReader(resourceAsStream,"UTF-8"));
+            for (String line = br.readLine(); line != null; line = br.readLine()) {
+                if (!StringUtil.isNullOrEmpty(line)) {
+                    sb.append(line);
+                    sb.append("\r\n");
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return sb.toString();
+    }
+
+    public static InputStream getFileStream(String fileName){
+        return GetStaticFileUtils.class.getResourceAsStream(fileName);
+    }
+
+
+}

+ 35 - 0
src/main/java/HandleTotalSummon.java

@@ -0,0 +1,35 @@
+import wei.yigulu.iec104.annotation.AsduType;
+import wei.yigulu.iec104.apdumodel.Apdu;
+import wei.yigulu.iec104.asdudataframe.TotalSummonType;
+import wei.yigulu.iec104.util.SendDataFrameHelper;
+
+
+/**
+ * 处理总召唤帧
+ *
+ * @author 修唯xiuwei
+ * @version 3.0
+ */
+@AsduType(typeId = 100)
+public class HandleTotalSummon extends TotalSummonType {
+
+
+    @Override
+    public byte[][] handleAndAnswer(Apdu apdu) throws Exception {
+        apdu.getLog().trace("----------响应总召唤---------");
+        //当该帧是由 SlaverBuilder 或其子类接收到的  且为总召唤原因为激活
+        if (apdu.getAsdu().getCot().getNot() == 6) {
+            //响应总召唤 激活确认帧
+            SendDataFrameHelper.sendTotalSummonFrame(apdu.getChannel(), apdu.getAsdu().getCommonAddress(), 7, apdu.getLog());
+            //响应遥信
+            SendDataFrameHelper.sendYxDataFrame(apdu.getChannel(), ProtocolDataContainer.getInstance().getBooleans(), apdu.getAsdu().getCommonAddress(), 20, apdu.getLog());
+            //响应遥测
+            SendDataFrameHelper.sendYcDataFrame(apdu.getChannel(), ProtocolDataContainer.getInstance().getNumbers(), apdu.getAsdu().getCommonAddress(), 20, apdu.getLog());
+            //响应总召唤 激活停止帧
+            SendDataFrameHelper.sendTotalSummonFrame(apdu.getChannel(), apdu.getAsdu().getCommonAddress(), 10, apdu.getLog());
+        }
+        return null;
+    }
+
+
+}

+ 28 - 0
src/main/java/HttpRequestHandler.java

@@ -0,0 +1,28 @@
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.http.*;
+
+public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
+
+    @Override
+    public void channelReadComplete(ChannelHandlerContext ctx) {
+        ctx.flush();
+    }
+
+    @Override
+    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
+        //100 Continue
+        if (HttpUtil.is100ContinueExpected(req)) {
+            ctx.write(new DefaultFullHttpResponse(
+                    HttpVersion.HTTP_1_1,
+                    HttpResponseStatus.CONTINUE));
+        }
+        // 获取请求的uri
+        String uri = req.uri();
+        QueryStringDecoder decoder = new QueryStringDecoder(uri);
+        FullHttpResponse response = RequestControllerManager.getInstance().getRequestHandler(req.method().name(), decoder.path()).handle(req);
+        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
+    }
+
+}

+ 40 - 0
src/main/java/HttpServer.java

@@ -0,0 +1,40 @@
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.logging.LoggingHandler;
+
+import java.net.InetSocketAddress;
+
+/**
+ * netty server
+ * 2018/11/1.
+ */
+public class HttpServer {
+
+    int port;
+
+    public HttpServer(int port) {
+        this.port = port;
+    }
+
+    public void start() throws Exception {
+        ServerBootstrap bootstrap = new ServerBootstrap();
+        EventLoopGroup boss = new NioEventLoopGroup();
+        EventLoopGroup work = new NioEventLoopGroup();
+        bootstrap.group(boss, work)
+                .channel(NioServerSocketChannel.class)
+                .childHandler(new HttpServerInitializer());
+        ChannelFuture f = bootstrap.bind(new InetSocketAddress(port)).sync();
+        System.out.println(" server start up on port : " + port);
+        f.channel().closeFuture().sync();
+
+    }
+
+}
+
+
+

+ 19 - 0
src/main/java/HttpServerInitializer.java

@@ -0,0 +1,19 @@
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpServerCodec;
+
+
+
+public class HttpServerInitializer extends ChannelInitializer<SocketChannel> {
+
+    @Override
+    protected void initChannel(SocketChannel channel) throws Exception {
+        ChannelPipeline pipeline = channel.pipeline();
+        pipeline.addLast(new HttpServerCodec());// http 编解码
+        pipeline.addLast("httpAggregator",new HttpObjectAggregator(512*1024)); // http 消息聚合器                                                                     512*1024为接收的最大contentlength
+        pipeline.addLast(new HttpRequestHandler());// 请求处理器
+
+    }
+}

+ 123 - 0
src/main/java/PointDataCalculator.java

@@ -0,0 +1,123 @@
+import com.alibaba.fastjson.JSON;
+import com.googlecode.aviator.AviatorEvaluator;
+import com.googlecode.aviator.Expression;
+import com.googlecode.aviator.Options;
+import com.googlecode.aviator.exception.ExpressionSyntaxErrorException;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+@Slf4j
+public class PointDataCalculator extends BasePointData {
+
+
+    private static final Pattern PAR_EXPR = Pattern.compile("P\\(\\d+\\)");
+
+    private static final Pattern RANDOM_EXPR = Pattern.compile("R\\((\\d+)?\\)");
+
+    private static final Pattern TIME_EXPR = Pattern.compile("T\\(\\)");
+
+
+    static {
+        AviatorEvaluator.setOption(Options.ALWAYS_PARSE_FLOATING_POINT_NUMBER_INTO_DECIMAL, true);
+    }
+
+    /**
+     * 计算表达式
+     */
+    Expression compiledExp;
+
+    /**
+     * 计算所需参数
+     */
+    private Map<String, Integer> calculatingParameters;
+
+    public PointDataCalculator(Integer point, String formula) {
+        super(point,formula);
+        getOrInitCompiledExp();
+    }
+
+    public Object calculate() {
+        Map<String, Object> values = new HashMap<>();
+        for (Map.Entry<String, Integer> e : getCalculatingParameters().entrySet()) {
+            values.put(e.getKey(), protocolDataContainer.getData(e.getValue()));
+        }
+        Object o = null;
+        try {
+            o=getOrInitCompiledExp().execute(values);
+        } catch (ArithmeticException e) {
+            log.error("除数为0,计算公式为:{},请检查数据池内是否有该点位是否有数据", getValue());
+        } catch (Exception e) {
+            log.error("数据计算{}时发生异常", getValue(), e);
+            log.info(JSON.toJSONString(values));
+            throw new RuntimeException("计算器计算数据时发生异常,公式为:" +getValue());
+        }
+        return o;
+    }
+
+
+    /**
+     * 对公式进行初始化  如果公式解析失败将会抛出异常
+     *
+     * @return Expression  解析完成的表达式
+     */
+    private Expression getOrInitCompiledExp() {
+        if (this.compiledExp == null) {
+            try {
+                String temS;
+                String temS2;
+                String temFormula = getValue();
+                Matcher m = RANDOM_EXPR.matcher(getValue());
+                while (m.find()) {
+                    temS = m.group();
+                    System.out.println(temS);
+                    temS2 = temS.replace("R", "rand");
+                    temFormula = temFormula.replace(temS, temS2);
+                }
+                m = TIME_EXPR.matcher(getValue());
+                while (m.find()) {
+                    temS = m.group();
+                    temS2 = temS.replace("T", "now");
+                    temFormula = temFormula.replace(temS, temS2);
+                }
+                System.out.println(temFormula);
+                this.compiledExp = AviatorEvaluator.compile(temFormula);
+            } catch (ExpressionSyntaxErrorException e) {
+                log.error("公式解析失败,公式不可用", e);
+                throw new RuntimeException("公式解析失败,公式不可用");
+            }
+        }
+        return this.compiledExp;
+    }
+
+    /**
+     * 对计算所需的参数进行整理
+     *
+     * @return P_(点位)_P  替换成真实的数据   字段--数值
+     */
+    public Map<String, Integer> getCalculatingParameters() {
+        if (this.calculatingParameters == null) {
+            this.calculatingParameters = new HashMap<>();
+            Matcher matcher = PAR_EXPR.matcher(getValue());
+            while (matcher.find()) {
+                String s = matcher.group();
+                s = s.substring(1, s.length() - 1);
+                System.out.println(s);
+                this.calculatingParameters.put("P_" + s + "_P", Integer.parseInt(s));
+            }
+        }
+        return this.calculatingParameters;
+    }
+
+
+    @Override
+    public void joinRealTimeData() {
+        protocolDataContainer.addCalculator(this);
+    }
+}

+ 21 - 0
src/main/java/PointDataConstantValue.java

@@ -0,0 +1,21 @@
+import java.math.BigDecimal;
+
+public class PointDataConstantValue extends BasePointData {
+
+
+
+    public PointDataConstantValue(Integer point, String value) {
+        super(point,value);
+
+
+    }
+
+    @Override
+    public void joinRealTimeData() {
+        if (getPoint() >= 16385) {
+            protocolDataContainer.putNumber(getPoint(), new BigDecimal(getValue()));
+        } else {
+            protocolDataContainer.putBoolean(getPoint(), Boolean.valueOf(getValue()));
+        }
+    }
+}

+ 47 - 0
src/main/java/PointDataContainer.java

@@ -0,0 +1,47 @@
+import com.alibaba.fastjson.JSONObject;
+
+import java.util.*;
+
+public class PointDataContainer {
+
+
+    private PointDataContainer() {
+    }
+
+    /**
+     * 获取单例实例
+     *
+     * @return the instance
+     */
+    public static final PointDataContainer getInstance() {
+        return PointDataContainer.LazyHolder.INSTANCE;
+    }
+
+
+    private static class LazyHolder {
+        private static final PointDataContainer INSTANCE = new PointDataContainer();
+    }
+
+
+    private Map<Integer, BasePointData> pointDataMap = new HashMap<>();
+
+
+    public void add2PointDataMap(BasePointData pointData){
+        this.pointDataMap.put(pointData.getPoint(),pointData);
+        pointData.joinRealTimeData();
+    }
+
+    public void deleteFromPointDataMap(Integer point){
+        this.pointDataMap.remove(point);
+        ProtocolDataContainer.getInstance().removeCalculatorTask(point);
+    }
+
+    public JSONObject getAllPoint(){
+        JSONObject jsonObject=new JSONObject();
+        for(Integer i: new TreeSet<>(this.pointDataMap.keySet())){
+            jsonObject.put(i+"",this.pointDataMap.get(i).getValue());
+        }
+        return jsonObject;
+    }
+
+}

+ 264 - 0
src/main/java/ProtocolDataContainer.java

@@ -0,0 +1,264 @@
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 协议采集到数据的容器
+ *
+ * @author 修唯xiuwei
+ * @version 3.0
+ */
+
+@Slf4j
+public class ProtocolDataContainer {
+
+
+    /**
+     * 遥测数据
+     */
+    private Map<Integer, BigDecimal> yc = new ConcurrentHashMap<>();
+
+    /**
+     * 遥信数据
+     */
+    private Map<Integer, Boolean> yx = new ConcurrentHashMap<>();
+    /**
+     * 点位数据是否是有效值
+     */
+    private Map<Integer, Boolean> isValid = new ConcurrentHashMap<>();
+
+    /**
+     * 点位计算器
+     */
+    private Map<Integer, PointDataCalculator> calculators = new ConcurrentHashMap<>();
+
+    /**
+     * 点位计算器的线程任务
+     */
+    private Map<Integer, Callable> calculatorTasks = new ConcurrentHashMap<>();
+
+    private ProtocolDataContainer() {
+    }
+
+    /**
+     * 获取单例实例
+     *
+     * @return the instance
+     */
+    public static final ProtocolDataContainer getInstance() {
+        return LazyHolder.INSTANCE;
+    }
+
+    /**
+     * 向池子内置数据
+     *
+     * @param point point
+     * @param value value
+     */
+    public void putNumber(Integer point, BigDecimal value) {
+        this.yc.put(point, value);
+    }
+
+    public Map<Integer, Number> getNumbers() {
+        Map<Integer, Number> numberMap=new HashMap<>();
+        this.yc.forEach((k,v)->numberMap.put(k,v));
+        return numberMap;
+    }
+
+    public Map<Integer, Boolean> getBooleans() {
+        Map<Integer, Boolean> booleanMap=new HashMap<>();
+        this.yx.forEach((k,v)->booleanMap.put(k,v));
+        return booleanMap;
+    }
+
+    /**
+     * 向池子内置数据
+     *
+     * @param point point
+     * @param value value
+     */
+    public void putBoolean(Integer point, Boolean value) {
+        this.yx.put(point, value);
+    }
+
+    /**
+     * 设定点位是否可用
+     *
+     * @param point point
+     * @param value value
+     */
+    public void setAvailable(Integer point, Boolean value) {
+        this.isValid.put(point, value);
+    }
+
+    /**
+     * 获取到缓存池内的数值 如果遥测池内没有但是存在于遥信池 将返回 0or1
+     *
+     * @param point 点位
+     * @return Number number
+     */
+    public BigDecimal getNumber(Integer point) {
+        if (isYcContains(point)) {
+            return this.yc.get(point);
+        } else if (isYxContains(point)) {
+            return this.yx.get(point) ? BigDecimal.valueOf(1D) : BigDecimal.valueOf(0D);
+        } else {
+            return BigDecimal.valueOf(-99D);
+        }
+    }
+
+    /**
+     * 获取到返回池内的遥信数据  如果遥信池内没有 遥测池内有 那么大于0的数值 将会返回ture 否则为false
+     *
+     * @param point 点位
+     * @return Boolean boolean
+     */
+    public Boolean getBoolean(Integer point) {
+        if (isYxContains(point)) {
+            return this.yx.get(point);
+        } else if (isYcContains(point)) {
+            return this.yc.get(point).compareTo(BigDecimal.ZERO) > 0;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 获取点位数据  直接返回
+     *
+     * @param point 点位key
+     * @return 返回值  可能是Boolean 或者是 BigDecimal
+     */
+    public Object getData(Integer point) {
+        if (isYcContains(point)) {
+            return this.yc.get(point);
+        }
+        if (isYxContains(point)) {
+            return this.yx.get(point);
+        }
+        return BigDecimal.valueOf(0);
+    }
+
+
+    /**
+     * 查看某点位数据是否可用
+     *
+     * @param point point
+     * @return the available
+     */
+    public Boolean getAvailable(Integer point) {
+        return this.isValid.get(point);
+    }
+
+    /**
+     * 遥信数据池是否含有该点位
+     *
+     * @param point point
+     * @return the boolean
+     */
+    public Boolean isYxContains(Integer point) {
+        return this.yx.containsKey(point);
+    }
+
+    /**
+     * 遥测数据池是否含有该点位值
+     *
+     * @param point point
+     * @return the boolean
+     */
+    public Boolean isYcContains(Integer point) {
+        return this.yc.containsKey(point);
+    }
+
+    /**
+     * 向计算器池子中  添加计算器
+     *
+     * @param point      点位id
+     * @param calculator 点位计算器
+     * @return 点位计算器
+     */
+    public PointDataCalculator addCalculator(Integer point, PointDataCalculator calculator) {
+        removeCalculatorTask(point);
+        this.calculators.put(point, calculator);
+        this.calculatorTasks.put(point, RecurringTaskContainer.getInstance().addRecurringTask(5, "计算器计算任务", () -> {
+            Object obj = null;
+            try {
+                obj = calculator.calculate();
+                if (obj != null) {
+                    if (obj instanceof Boolean) {
+                        this.putBoolean(point, (Boolean) obj);
+                    } else {
+                        this.putNumber(point, new BigDecimal(obj.toString()));
+                    }
+                } else {
+                    log.warn("计算公式{}结果值为空", calculator.getValue());
+                }
+            } catch (Exception ex) {
+                log.error("数据计算器池计算异常", ex);
+            }
+            return obj;
+        }));
+        return calculator;
+    }
+
+
+    /**
+     * 移除点位计算线程任务
+     *
+     * @param point 点位id
+     */
+    public void removeCalculatorTask(Integer point) {
+        if (this.calculatorTasks.containsKey(point)) {
+            RecurringTaskContainer.getInstance().removeTask(this.calculatorTasks.get(point));
+            this.calculatorTasks.remove(point);
+        }
+    }
+
+
+    /**
+     * 移除所有的点位计算线程任务
+     */
+    public void removeAllCalculatorTask() {
+        this.calculatorTasks.forEach((k, v) -> RecurringTaskContainer.getInstance().removeTask(v));
+        this.calculatorTasks.clear();
+    }
+
+    /**
+     * 添加计算器
+     * 向计算器池子中  添加计算器
+     *
+     * @return {@link PointDataCalculator}
+     */
+    public PointDataCalculator addCalculator(PointDataCalculator pointDataCalculator) {
+        return addCalculator(pointDataCalculator.getPoint(), pointDataCalculator);
+    }
+
+
+    @Override
+    public String toString() {
+        JSONObject object = new JSONObject();
+        object.put("yx", this.yx);
+        object.put("yc", this.yc);
+        return object.toJSONString();
+    }
+
+    public JSONObject getValues() {
+        JSONObject jsonObject = new JSONObject();
+        this.yc.forEach((k, v) -> jsonObject.put(k.toString(), v.setScale(2, RoundingMode.HALF_UP)));
+        this.yx.forEach((k, v) -> jsonObject.put(k.toString(), v));
+        return jsonObject;
+    }
+
+
+    private static class LazyHolder {
+        private static final ProtocolDataContainer INSTANCE = new ProtocolDataContainer();
+    }
+
+
+}

+ 194 - 0
src/main/java/RecurringTaskContainer.java

@@ -0,0 +1,194 @@
+
+import lombok.extern.slf4j.Slf4j;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.*;
+
+/**
+ * 循环反复任务池
+ * 会有线程查看线程任务执行时间 不允许有执行时间超过两秒的线程
+ * 最小执行间隔时间为3秒 因为 检查任务线程的频率为2s
+ *
+ * @author: xiuwei
+ * @version:
+ */
+@Slf4j
+public class RecurringTaskContainer {
+
+  /**
+   * 核心数量为10 排序数量为100 默认排序规则  多余线程存活时间为60秒的 定长线程
+   * 用于执行定时任务
+   */
+  private static final ExecutorService calculatorThread = new ThreadPoolExecutor(10, 200,
+    60L, TimeUnit.SECONDS, new SynchronousQueue<>());
+
+  /**
+   * 核心运行数量为4的定时任务池 用于拉起定时任务
+   */
+  private static final ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4);
+
+  /**
+   * 定时任务对象  以时间间隔进行区分
+   */
+  private Map<Integer, Map<Callable, String>> tasks = new ConcurrentHashMap();
+  /**
+   * 在线程池内正在执行的任务  以时间间隔进行区分
+   */
+  private Map<Integer, Map<Future, String>> futures = new ConcurrentHashMap();
+
+  /**
+   * 单例
+   *
+   * @return 实例
+   */
+  public static final RecurringTaskContainer getInstance() {
+    return LazyHolder.INSTANCE;
+  }
+
+  /**
+   * 向定时任务集合里添加定时任务
+   *
+   * @param interval 时间间隔
+   * @param task     任务
+   * @return 返回添加的任务
+   */
+  public <T> Callable addRecurringCallable(Integer interval, String describe, Callable<T> task) {
+    if (interval < 2) {
+      throw new RuntimeException("该工具不允许执行3s以下间隔的任务");
+    }
+    log.info("循环任务池添加任务-{}", describe);
+    if (getTasks().containsKey(interval)) {
+      getTasks().get(interval).put(task, describe);
+    } else {
+      Map<Callable, String> map = new ConcurrentHashMap();
+      map.put(task, describe);
+      getTasks().put(interval, map);
+      futures.put(interval, new ConcurrentHashMap());
+      startScheduledTask(interval);
+    }
+    return task;
+  }
+
+  /**
+   * 向定时任务集合里添加定时任务
+   *
+   * @param interval 时间间隔
+   * @param task     任务
+   * @return 返回添加的任务
+   */
+  public <T> Callable addRecurringTask(Integer interval, String describe, Task4Recurring<T> task) {
+    if (interval < 2) {
+      throw new RuntimeException("该工具不允许执行3s以下间隔的任务");
+    }
+    Callable<T> callable = () -> {
+      T o;
+      try {
+        o = task.task();
+      } catch (Exception e) {
+        log.error("定时任务本身产生异常", e);
+        throw e;
+      }
+      return o;
+    };
+    return addRecurringCallable(interval, describe, callable);
+  }
+
+  /**
+   * 移除定时任务
+   *
+   * @param interval 时间间隔
+   * @param task     执行的任务线程
+   */
+  public void removeTask(Integer interval, Callable task) {
+    if (this.getTasks().containsKey(interval)) {
+      this.getTasks().get(interval).remove(task);
+    }
+  }
+
+  private synchronized Map<Integer, Map<Callable, String>> getTasks() {
+    return this.tasks;
+  }
+
+  /**
+   * 移除定时任务
+   *
+   * @param task 任务线程
+   */
+  public void removeTask(Callable task) {
+    for (Integer key : getTasks().keySet()) {
+      if (this.getTasks().get(key).containsKey(task)) {
+        log.info("循环任务池移除任务-{}", this.getTasks().get(key).get(task));
+        this.getTasks().get(key).remove(task);
+        break;
+      }
+    }
+  }
+
+  /**
+   * 开启定时任务线程 用于拉起具体的定时任务
+   *
+   * @param interval 时间间隔
+   */
+  public void startScheduledTask(Integer interval) {
+    scheduledThreadPool.scheduleAtFixedRate(() -> {
+      Map<Callable, String> map = getTasks().get(interval);
+      futures.put(interval, new ConcurrentHashMap());
+      Future future = null;
+      for (Callable c : map.keySet()) {
+        try {
+          //log.info("提交循环任务到执行器-{}",map.get(c));
+          future = calculatorThread.submit(c);
+          futures.get(interval).put(future, map.get(c));
+        } catch (Exception e) {
+          if (future != null) {
+            futures.get(interval).remove(future);
+          }
+          if(e instanceof RejectedExecutionException){
+            log.error("提交任务过多超过排序队列长度,任务被拒绝");
+          }else{
+          log.error("执行循环任务时发生异常", e);}
+        }
+        future = null;
+      }
+    }, 0, interval, TimeUnit.SECONDS);
+    scheduledThreadPool.scheduleAtFixedRate(() -> {
+      Map<Future, String> map = futures.get(interval);
+      Iterator<Future> iter = map.keySet().iterator();
+      Future future;
+      while (iter.hasNext()) {
+        try {
+          future = iter.next();
+          if (future.isCancelled()) {
+            log.info("清除已取消的任务{},删除该任务", map.get(future));
+            map.remove(future);
+            continue;
+          }
+          if (!future.isDone()) {
+            log.info("发现未完成的超时任务{},取消该任务", map.get(future));
+            future.cancel(false);
+          }
+        } catch (Exception e) {
+          e.printStackTrace();
+        }
+      }
+    }, interval - 1, interval, TimeUnit.SECONDS);
+  }
+
+  /**
+   * 它的意义就是为了打印一下日志 就是为了包裹一层try catch
+   *
+   * @param <T>
+   */
+  public interface Task4Recurring<T> {
+    T task() throws Exception;
+  }
+
+  /**
+   * 为了单例的内部类
+   *
+   * @return 实例
+   */
+  private static class LazyHolder {
+    private static final RecurringTaskContainer INSTANCE = new RecurringTaskContainer();
+  }
+}

+ 115 - 0
src/main/java/RequestController.java

@@ -0,0 +1,115 @@
+import com.alibaba.fastjson.JSON;
+import io.netty.handler.codec.http.QueryStringDecoder;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 请求控制器
+ *
+ * @author xiuwei
+ * @date 2021/11/26
+ */
+public class RequestController {
+
+
+    private static class LazyHolder {
+        private static final RequestController INSTANCE = new RequestController();
+    }
+
+    public static final RequestController getInstance() {
+        return RequestController.LazyHolder.INSTANCE;
+    }
+
+    private RequestController() {
+    }
+
+    @RequestHandlerFunction(requestType = RequestControllerManager.RequestMethodType.GET, requestUrl = "/addConstantPoint")
+    public RequestHandler addConstantPoint() {
+        return (request) -> {
+            QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
+            Map<String, List<String>> params = decoder.parameters();
+            String point = null;
+            String value = null;
+            if (params.containsKey("point")) {
+                point = params.get("point").get(0);
+            }
+            if (params.containsKey("value")) {
+                value = params.get("value").get(0);
+            }
+            if (point != null && value != null) {
+                PointDataContainer.getInstance().add2PointDataMap(new PointDataConstantValue(Integer.valueOf(point), value));
+                return addConstantPoint().getTextResponse("ok");
+            } else {
+               return  addConstantPoint().getTextResponse("err");
+            }
+        };
+    }
+
+    @RequestHandlerFunction(requestType = RequestControllerManager.RequestMethodType.GET, requestUrl = "/addCalculatorPoint")
+    public RequestHandler addCalculatorPoint() {
+        return (request) -> {
+            QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
+            Map<String, List<String>> params = decoder.parameters();
+            String point = null;
+            String value = null;
+            if (params.containsKey("point")) {
+                point = params.get("point").get(0);
+            }
+            if (params.containsKey("value")) {
+                value = params.get("value").get(0);
+            }
+            if (point != null && value != null) {
+                PointDataContainer.getInstance().add2PointDataMap(new PointDataCalculator(Integer.valueOf(point), value));
+                return addCalculatorPoint().getTextResponse("ok");
+            } else {
+                return  addCalculatorPoint().getTextResponse("err");
+            }
+        };
+    }
+
+
+    @RequestHandlerFunction(requestType = RequestControllerManager.RequestMethodType.GET, requestUrl = "/deletePoint")
+    public RequestHandler deletePoint() {
+        return (request) -> {
+            QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
+            Map<String, List<String>> params = decoder.parameters();
+            String point = null;
+            if (params.containsKey("point")) {
+                point = params.get("point").get(0);
+            }
+            if (point != null) {
+                PointDataContainer.getInstance().deleteFromPointDataMap(Integer.valueOf(point));
+                return addCalculatorPoint().getTextResponse("ok");
+            } else {
+                return  addCalculatorPoint().getTextResponse("err");
+            }
+        };
+    }
+
+
+
+    @RequestHandlerFunction(requestType = RequestControllerManager.RequestMethodType.GET, requestUrl = "/")
+    public RequestHandler index() {
+        return (request) -> index().getTextResponse(GetStaticFileUtils.getFileContext("slaver.html"));
+    }
+
+
+    @RequestHandlerFunction(requestType = RequestControllerManager.RequestMethodType.GET, requestUrl = "/getAllPoints")
+    public RequestHandler getAllPoints() {
+        return (request) -> index().getTextResponse(PointDataContainer.getInstance().getAllPoint().toJSONString());
+    }
+
+    @RequestHandlerFunction(requestType = RequestControllerManager.RequestMethodType.GET, requestUrl = "/favicon.ico")
+    public RequestHandler icon() {
+        return (request) -> icon().getIcon(GetStaticFileUtils.getFileStream("favicon.ico"));
+    }
+
+
+    @RequestHandlerFunction(requestType = RequestControllerManager.RequestMethodType.GET, requestUrl = "/getValues")
+    public RequestHandler getValues() {
+        return (request) -> getValues().getTextResponse(ProtocolDataContainer.getInstance().getValues().toJSONString());
+    }
+
+
+}

+ 78 - 0
src/main/java/RequestControllerManager.java

@@ -0,0 +1,78 @@
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import lombok.extern.slf4j.Slf4j;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author: xiuwei
+ * @version:
+ */
+@Slf4j
+public class RequestControllerManager {
+
+    private static class LazyHolder {
+        private static final RequestControllerManager INSTANCE = new RequestControllerManager();
+    }
+
+    public static final RequestControllerManager getInstance() {
+        return RequestControllerManager.LazyHolder.INSTANCE;
+    }
+
+    private RequestControllerManager() {
+        init();
+    }
+
+
+    private final Map<String, Map<String, RequestHandler>> requestHandlerMap = new HashMap<>();
+
+    private void addRequestHandler(String requestType, String requestUrl, RequestHandler requestHandler) {
+        if (!this.requestHandlerMap.containsKey(requestType)) {
+            this.requestHandlerMap.put(requestType, new HashMap<>());
+        }
+        this.requestHandlerMap.get(requestType).put(requestUrl, requestHandler);
+    }
+    public RequestHandler getRequestHandler(String requestType, String requestUrl){
+        if(this.requestHandlerMap.containsKey(requestType) && this.requestHandlerMap.get(requestType).containsKey(requestUrl)){
+            return this.requestHandlerMap.get(requestType).get(requestUrl);
+        }else{
+            return new RequestHandler() {
+                @Override
+                public FullHttpResponse handle(FullHttpRequest request) {
+                    return getTextResponse("404");
+                }
+            };
+        }
+    }
+
+    private void init()  {
+        Method[] functions = RequestController.class.getDeclaredMethods();
+        for(Method m: functions){
+            RequestHandlerFunction t = m.getAnnotation(RequestHandlerFunction.class);
+            if(t!=null && m.getReturnType().equals(RequestHandler.class) && m.getParameterCount()==0) {
+                try {
+                    addRequestHandler(t.requestType().toString(),t.requestUrl(),(RequestHandler)m.invoke(RequestController.getInstance()));
+                } catch (IllegalAccessException e) {
+                    e.printStackTrace();
+                } catch (InvocationTargetException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    public enum RequestMethodType{
+        GET,POST;
+        public String toString(){
+            switch (this){
+                case GET:return "GET";
+                case POST: return "POST";
+            }
+            return "GET";
+        }
+    }
+
+}

+ 40 - 0
src/main/java/RequestHandler.java

@@ -0,0 +1,40 @@
+import io.netty.buffer.Unpooled;
+import io.netty.handler.codec.http.*;
+import io.netty.util.CharsetUtil;
+import sun.misc.IOUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+@FunctionalInterface
+public interface RequestHandler {
+     FullHttpResponse handle(FullHttpRequest request);
+
+      default FullHttpResponse getTextResponse(String pageMsg){
+         // 创建http响应
+         FullHttpResponse response = new DefaultFullHttpResponse(
+                 HttpVersion.HTTP_1_1,
+                 HttpResponseStatus.OK,
+                 Unpooled.copiedBuffer(pageMsg, CharsetUtil.UTF_8));
+         // 设置头信息
+         response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
+         return  response;
+     }
+
+
+    default FullHttpResponse getIcon(InputStream icon)  {
+        // 创建http响应
+        FullHttpResponse response = null;
+        try {
+            response = new DefaultFullHttpResponse(
+                    HttpVersion.HTTP_1_1,
+                    HttpResponseStatus.OK,
+                    Unpooled.copiedBuffer(IOUtils.readFully(icon,icon.available(),false)));
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        // 设置头信息
+        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "image/x-icon");
+        return  response;
+    }
+}

+ 16 - 0
src/main/java/RequestHandlerFunction.java

@@ -0,0 +1,16 @@
+import io.netty.handler.codec.http.HttpMethod;
+
+import java.lang.annotation.*;
+
+
+@Target(ElementType.METHOD)
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RequestHandlerFunction {
+
+
+    RequestControllerManager.RequestMethodType requestType() default RequestControllerManager.RequestMethodType.GET;
+
+    String requestUrl();
+
+}

+ 54 - 0
src/main/java/StartHttpServer.java

@@ -0,0 +1,54 @@
+import io.netty.channel.Channel;
+import wei.yigulu.iec104.nettyconfig.Iec104SlaverBuilder;
+import wei.yigulu.iec104.util.SendDataFrameHelper;
+
+public class StartHttpServer {
+    public static void main(String[] args) throws Exception {
+        //104端口
+        Integer port1 = 2404;
+        //页面端口
+        Integer port2 = 2405;
+        Integer tem1;
+        Integer tem2;
+        if (args.length == 1 || args.length > 2) {
+            throw new RuntimeException("请输入两个端口,参数1为104Slver端口,参数2为配置页面端口");
+        }
+        if (args.length == 2) {
+            try {
+                tem1 = Integer.parseInt(args[0]);
+                if (tem1 > 65535 || tem1 < 0) {
+                    throw new RuntimeException("参数1应为0-65535整数");
+                }
+            } catch (Exception e) {
+                throw new RuntimeException("参数1应为0-65535整数");
+            }
+            try {
+                tem2 = Integer.parseInt(args[1]);
+                if (tem1 > 65535 || tem1 < 0) {
+                    throw new RuntimeException("参数2应为0-65535整数");
+                }
+            } catch (Exception e) {
+                throw new RuntimeException("参数2应为0-65535整数");
+            }
+            if (tem1.intValue() == tem2.intValue()) {
+                throw new RuntimeException("参数1与参数2不能相同");
+            }
+            port1 = tem1;
+            port2 = tem2;
+        }
+        Iec104SlaverBuilder iec104SlaverBuilder = new Iec104SlaverBuilder(port1);
+        iec104SlaverBuilder.createByUnBlock();
+        RecurringTaskContainer.getInstance().addRecurringTask(60, "向对端发送突发上送", () -> {
+            if (iec104SlaverBuilder.getChannels().size() > 0) {
+                for (Channel c : iec104SlaverBuilder.getChannels()) {
+                    SendDataFrameHelper.sendYxDataFrameDiscontinuity(c, ProtocolDataContainer.getInstance().getBooleans(), 1, 3, iec104SlaverBuilder.getLog());
+                    SendDataFrameHelper.sendYcDataFrameDiscontinuity(c, ProtocolDataContainer.getInstance().getNumbers(), 1, 3, iec104SlaverBuilder.getLog());
+                }
+            }
+            return null;
+        });
+        HttpServer httpServer = new HttpServer(port2);
+        httpServer.start();
+    }
+
+}

二進制
src/main/resources/favicon.ico


+ 40 - 0
src/main/resources/logback.xml

@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="60 seconds" debug="false">
+    <property name="APP_Name" value="104SLAVER"/>
+    <property name="Log_Dir" value="./"/>
+    <contextName>${APP_Name}</contextName>
+
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder charset="UTF-8">
+            <pattern>${APP_Name}-%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <appender name="InfoFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${Log_Dir}/${APP_Name}/logs/info.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <FileNamePattern>${Log_Dir}/logs/${APP_Name}/%d{yyyy-MM-dd}/info.%i.log</FileNamePattern>
+            <MaxHistory>30</MaxHistory>
+            <maxFileSize>100MB</maxFileSize>
+            <totalSizeCap>2GB</totalSizeCap>
+        </rollingPolicy>
+        <encoder charset="UTF-8" class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+
+    <!--<appender name="SocketError" class="com.hrhx.logger.SocketAppend"></appender>-->
+
+
+    <!--子节点<root>:它也是<loger>元素,但是它是根loger,是所有<loger>的上级。
+    只有一个level属性,因为name已经被命名为"root",且已经是最上级了。
+  level: 用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL和OFF,
+    不能设置为INHERITED或者同义词NULL。 默认是DEBUG。-->
+    <root level="DEBUG">
+        <appender-ref ref="STDOUT"/>
+        <appender-ref ref="InfoFile"/>
+        <!--<appender-ref ref="SocketE-->
+    </root>
+
+</configuration>

+ 326 - 0
src/main/resources/slaver.html

@@ -0,0 +1,326 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>104Slaver</title>
+</head>
+<style>
+    td {
+        width: 90px;
+        text-align: center;
+        border: 1px black solid;
+    }
+
+    td:nth-child(2), td:nth-child(4), td:nth-child(6), td:nth-child(8) {
+        background: #ff0000;
+    }
+
+    td:nth-child(1), td:nth-child(3), td:nth-child(5), td:nth-child(7) {
+        background: #23B72BFF;
+    }
+
+    .th td {
+        background: white !important;
+    }
+</style>
+<body>
+<div>
+    <table style="margin-left: 10px">
+        <tbody id="wt">
+        <tr class="th">
+            <td style="border: none">端口名:</td>
+            <td style="border: none"><input style="width: 70px" type="text"></td>
+            <td style="border: none"><input type="button" value="开启监听"></td>
+            <td style="border: none"><input type="button" value="导出配置"></td>
+        </tr>
+        <tr style="height: 10px">
+            <td colspan="8" style="text-align: left;color: red;background-color: white">
+                16385(4001H)之后的点位默认遥测,之前的点位默认遥信
+            </td>
+        </tr>
+        <tr style="height: 10px">
+            <td colspan="8" style="text-align: left;color: red;background-color: white">
+                <ul style="margin: 0">
+                    输入公式时,须保证公式的准确性,遥信保证结果为布尔值,遥测保证结果为数值
+                    <li>"T()" 代表当前时间戳</li>
+                    <li>"R()" 代表0-1的浮点值</li>
+                    <li>"R(n)" 代表0-n的长整值</li>
+                    <li>"P(n)" 代表当下n点位的值</li>
+                </ul>
+            </td>
+        </tr>
+        <tr>
+            <td>点位</td>
+            <td>值</td>
+            <td>点位</td>
+            <td>值</td>
+            <td>点位</td>
+            <td>值</td>
+            <td>点位</td>
+            <td>值</td>
+        </tr>
+        </tbody>
+    </table>
+    <input style="margin: 10px 10px" type="button" value="+" id="addBut">
+    <input style="margin: 10px 10px" type="button" value="保存" id="save">
+
+</div>
+</body>
+</html>
+<script>
+    function ajax() {
+        var ajaxData = {
+            type: arguments[0].type || "GET",
+            url: arguments[0].url || "",
+            async: arguments[0].async || "true",
+            data: arguments[0].data || null,
+            dataType: arguments[0].dataType || "text",
+            contentType: arguments[0].contentType || "application/x-www-form-urlencoded",
+            beforeSend: arguments[0].beforeSend || function () {
+            },
+            success: arguments[0].success || function () {
+            },
+            error: arguments[0].error || function () {
+            }
+        }
+        ajaxData.beforeSend()
+        var xhr = createxmlHttpRequest();
+        xhr.responseType = ajaxData.dataType;
+        xhr.open(ajaxData.type, ajaxData.url, ajaxData.async);
+        xhr.setRequestHeader("Content-Type", ajaxData.contentType);
+        xhr.setRequestHeader("Access-Control-Allow-Origin", "*");
+        xhr.send(convertData(ajaxData.data));
+        xhr.onreadystatechange = function () {
+            if (xhr.readyState == 4) {
+                if (xhr.status == 200) {
+                    ajaxData.success(xhr.response)
+                } else {
+                    ajaxData.error()
+                }
+            }
+        }
+    }
+
+    function createxmlHttpRequest() {
+        if (window.ActiveXObject) {
+            return new ActiveXObject("Microsoft.XMLHTTP");
+        } else if (window.XMLHttpRequest) {
+            return new XMLHttpRequest();
+        }
+    }
+
+    function convertData(data) {
+        if (typeof data === 'object') {
+            var convertResult = "";
+            for (var c in data) {
+                convertResult += c + "=" + data[c] + "&";
+            }
+            convertResult = convertResult.substring(0, convertResult.length - 1)
+            return convertResult;
+        } else {
+            return data;
+        }
+    }
+
+    var wTable = document.getElementById("wt")
+    var isInput = false;
+
+    function addPoint() {
+        if (isInput) {
+            return
+        }
+        var lastTr = wTable.lastElementChild
+        if (lastTr.childElementCount > 6) {
+            wTable.appendChild(document.createElement("tr"))
+        }
+        wTable.lastElementChild.appendChild(getTd(getInputPointEle()))
+        wTable.lastElementChild.appendChild(getTd(getInputValueEle()))
+        isInput = true
+    }
+
+
+
+    function getTd(child) {
+        var td=document.createElement("td")
+        td.appendChild(child)
+        return td
+
+    }
+
+    function getInputPointEle(){
+        var inputE = document.createElement("input")
+        inputE.type = "text"
+        inputE.placeholder = "输入值或公式"
+        inputE.style.cssText = 'width: 82px';
+        inputE.classList.add("open-point")
+        inputE.onchange = checkValue;
+        return inputE
+    }
+
+    function getInputValueEle(){
+        var inputE = document.createElement("input")
+        inputE.type = "text"
+        inputE.placeholder = "输入值或公式"
+        inputE.style.cssText = 'width: 82px';
+        inputE.classList.add("open-value")
+        inputE.onchange = checkValue;
+        return inputE
+    }
+
+    function checkValue() {
+        var valueE = document.getElementsByClassName("open-value")[0];
+        var value = valueE.value
+        if (value == "true" || value == "false" || /^(-?\d+)(\.\d+)?$/.test(value)) {
+            // 固定值
+            return 1;
+        } else {
+            //公式
+            return 2;
+        }
+    }
+
+    function checkPoint() {
+        var pointE = document.getElementsByClassName("open-point")[0];
+        var point = pointE.value
+        if (!(/^[1-9]\d*$/.test(point))) {
+            console.log('输入的不是正整数');
+            pointE.focus();
+            return null;
+        } else {
+            return point
+        }
+    }
+
+    function getValues() {
+        ajax({
+            type: "GET",
+            url: "/getValues",
+            success: function (msg) {
+                var ps = JSON.parse(msg)
+                for (var p in ps) {
+                    if( document.getElementById("td_" + p)!=null){
+                    document.getElementById("td_" + p).innerText = ps[p]
+                    }
+                }
+            },
+            error: function () {
+            }
+        })
+    }
+
+    function buildPage() {
+        ajax({
+            type: "GET",
+            url: "/getAllPoints",
+            success: function (msg) {
+                var points = JSON.parse(msg)
+                var tr;
+                var td;
+                var i=0;
+                for (var p in points) {
+                    if (i % 4 == 0) {
+                        tr = wTable.appendChild(document.createElement("tr"))
+                    }
+                    td = document.createElement("td");
+                    td.id = td.id = "tdp_" + p
+                    td.innerHTML = p;
+                    td.ondblclick=dbChickOnPoint
+                    tr.appendChild(td);
+                    td = document.createElement("td");
+                    td.id = "td_" + p
+                    td.title=points[p]
+                    td.ondblclick=dbChickOnValue
+                    tr.appendChild(td);
+                    i++;
+                }
+            },
+            error: function () {
+                alert("error")
+            }
+        })
+    }
+
+    buildPage();
+
+    setInterval(getValues, 10);
+
+    function savePoint() {
+        var  pointE=document.getElementsByClassName("open-point")[0];
+        var  valueE=document.getElementsByClassName("open-value")[0];
+        if(pointE==null ){
+            return;
+        }
+        if (checkPoint() != null) {
+            if (checkValue() == 1) {
+                ajax({
+                    type: "GET",
+                    url: "/addConstantPoint?point=" + checkPoint() + "&value=" +escape(valueE.value).replace(/\+/g,'%2B'),
+                    success: function (msg) {
+                        location.reload();
+                    },
+                    error: function () {
+                        console.log("error")
+                    }
+                })
+            }else{
+                ajax({
+                    type: "GET",
+                    url: "/addCalculatorPoint?point=" + checkPoint() + "&value=" + escape(valueE.value).replace(/\+/g,'%2B'),
+                    success: function (msg) {
+                        location.reload();
+                    },
+                    error: function () {
+                        console.log("error")
+                    }
+                })
+            }
+        }else{
+            if(pointE.id==null ||pointE.id==""){
+                var grand=pointE.parentNode.parentNode
+                grand.removeChild(pointE.parentNode)
+                grand.removeChild(valueE.parentNode)
+                isInput = false;
+                return;
+            }else{
+                ajax({
+                    type: "GET",
+                    url: "/deletePoint?point=" + pointE.id.substr(6,pointE.id.length-6) ,
+                    success: function (msg) {
+                        location.reload();
+                    },
+                    error: function () {
+                        console.log("error")
+                    }
+                })
+            }
+        }
+    }
+    function dbChickOnPoint(){
+        modifyPoint(this.id.substr(4,this.id.length-4))
+    }
+    function dbChickOnValue(){
+        modifyPoint(this.id.substr(3,this.id.length-3))
+    }
+    function  modifyPoint(point){
+        if(!isInput){
+            var fe =document.getElementById("tdp_"+point);
+            var ch=getInputPointEle();
+            fe.innerText=""
+            fe.appendChild(ch);
+            ch.value=point
+            ch.id="input_"+point;
+            fe =document.getElementById("td_"+point);
+            ch=getInputValueEle();
+            ch.value=fe.title;
+            fe.innerText=""
+            fe.id=""
+            fe.appendChild(ch);
+            isInput=true;
+        }
+    }
+
+    document.getElementById("addBut").addEventListener("click", addPoint);
+    document.getElementById("save").addEventListener("click", savePoint);
+
+
+</script>