a.y)}function m(a,b,c,d,e){var f=-3*b+9*c-9*d+3*e,g=a*f+6*b-12*c+6*d;return a*g-3*b+3*c}function n(a,b,c,d,e,f,g,h,i){null==i&&(i=1),i=i>1?1:0>i?0:i;for(var j=i/2,k=12,l=[-.1252,.1252,-.3678,.3678,-.5873,.5873,-.7699,.7699,-.9041,.9041,-.9816,.9816],n=[.2491,.2491,.2335,.2335,.2032,.2032,.1601,.1601,.1069,.1069,.0472,.0472],o=0,p=0;k>p;p++){var q=j*l[p]+j,r=m(q,a,c,e,g),s=m(q,b,d,f,h),t=r*r+s*s;o+=n[p]*N.sqrt(t)}return j*o}function o(a,b,c,d,e,f,g,h,i){if(!(0>i||n(a,b,c,d,e,f,g,h)o;)l/=2,m+=(i>j?1:-1)*l,j=n(a,b,c,d,e,f,g,h,m);return m}}function p(a,b,c,d,e,f,g,h){if(!(Q(a,c)Q(e,g)||Q(b,d)
Q(f,h))){var i=(a*d-b*c)*(e-g)-(a-c)*(e*h-f*g),j=(a*d-b*c)*(f-h)-(b-d)*(e*h-f*g),k=(a-c)*(f-h)-(b-d)*(e-g);if(k){var l=i/k,m=j/k,n=+l.toFixed(2),o=+m.toFixed(2);if(!(n<+P(a,c).toFixed(2)||n>+Q(a,c).toFixed(2)||n<+P(e,g).toFixed(2)||n>+Q(e,g).toFixed(2)||o<+P(b,d).toFixed(2)||o>+Q(b,d).toFixed(2)||o<+P(f,h).toFixed(2)||o>+Q(f,h).toFixed(2)))return{x:l,y:m}}}}function q(a,b,c){var d=j(a),e=j(b);if(!l(d,e))return c?0:[];for(var f=n.apply(0,a),g=n.apply(0,b),h=~~(f/8),k=~~(g/8),m=[],o=[],q={},r=c?0:[],s=0;h+1>s;s++){var t=i.apply(0,a.concat(s/h));m.push({x:t.x,y:t.y,t:s/h})}for(s=0;k+1>s;s++)t=i.apply(0,b.concat(s/k)),o.push({x:t.x,y:t.y,t:s/k});for(s=0;h>s;s++)for(var u=0;k>u;u++){var v=m[s],w=m[s+1],x=o[u],y=o[u+1],z=S(w.x-v.x)<.001?"y":"x",A=S(y.x-x.x)<.001?"y":"x",B=p(v.x,v.y,w.x,w.y,x.x,x.y,y.x,y.y);if(B){if(q[B.x.toFixed(4)]==B.y.toFixed(4))continue;q[B.x.toFixed(4)]=B.y.toFixed(4);var C=v.t+S((B[z]-v[z])/(w[z]-v[z]))*(w.t-v.t),D=x.t+S((B[A]-x[A])/(y[A]-x[A]))*(y.t-x.t);C>=0&&1>=C&&D>=0&&1>=D&&(c?r++:r.push({x:B.x,y:B.y,t1:C,t2:D}))}}return r}function r(a,b){return t(a,b)}function s(a,b){return t(a,b,1)}function t(a,b,c){a=E(a),b=E(b);for(var d,e,f,g,h,i,j,k,l,m,n=c?0:[],o=0,p=a.length;p>o;o++){var r=a[o];if("M"==r[0])d=h=r[1],e=i=r[2];else{"C"==r[0]?(l=[d,e].concat(r.slice(1)),d=l[6],e=l[7]):(l=[d,e,d,e,h,i,h,i],d=h,e=i);for(var s=0,t=b.length;t>s;s++){var u=b[s];if("M"==u[0])f=j=u[1],g=k=u[2];else{"C"==u[0]?(m=[f,g].concat(u.slice(1)),f=m[6],g=m[7]):(m=[f,g,f,g,j,k,j,k],f=j,g=k);var v=q(l,m,c);if(c)n+=v;else{for(var w=0,x=v.length;x>w;w++)v[w].segment1=o,v[w].segment2=s,v[w].bez1=l,v[w].bez2=m;n=n.concat(v)}}}}}return n}function u(a,b,c){var d=v(a);return k(d,b,c)&&t(a,[["M",b,c],["H",d.x2+10]],1)%2==1}function v(a){var b=c(a);if(b.bbox)return J(b.bbox);if(!a)return d();a=E(a);for(var e,f=0,g=0,h=[],i=[],j=0,k=a.length;k>j;j++)if(e=a[j],"M"==e[0])f=e[1],g=e[2],h.push(f),i.push(g);else{var l=D(f,g,e[1],e[2],e[3],e[4],e[5],e[6]);h=h.concat(l.min.x,l.max.x),i=i.concat(l.min.y,l.max.y),f=e[5],g=e[6]}var m=P.apply(0,h),n=P.apply(0,i),o=Q.apply(0,h),p=Q.apply(0,i),q=d(m,n,o-m,p-n);return b.bbox=J(q),q}function w(a,b,c,d,f){if(f)return[["M",+a+ +f,b],["l",c-2*f,0],["a",f,f,0,0,1,f,f],["l",0,d-2*f],["a",f,f,0,0,1,-f,f],["l",2*f-c,0],["a",f,f,0,0,1,-f,-f],["l",0,2*f-d],["a",f,f,0,0,1,f,-f],["z"]];var g=[["M",a,b],["l",c,0],["l",0,d],["l",-c,0],["z"]];return g.toString=e,g}function x(a,b,c,d,f){if(null==f&&null==d&&(d=c),a=+a,b=+b,c=+c,d=+d,null!=f)var g=Math.PI/180,h=a+c*Math.cos(-d*g),i=a+c*Math.cos(-f*g),j=b+c*Math.sin(-d*g),k=b+c*Math.sin(-f*g),l=[["M",h,j],["A",c,c,0,+(f-d>180),0,i,k]];else l=[["M",a,b],["m",0,-d],["a",c,d,0,1,1,0,2*d],["a",c,d,0,1,1,0,-2*d],["z"]];return l.toString=e,l}function y(b){var d=c(b),g=String.prototype.toLowerCase;if(d.rel)return f(d.rel);a.is(b,"array")&&a.is(b&&b[0],"array")||(b=a.parsePathString(b));var h=[],i=0,j=0,k=0,l=0,m=0;"M"==b[0][0]&&(i=b[0][1],j=b[0][2],k=i,l=j,m++,h.push(["M",i,j]));for(var n=m,o=b.length;o>n;n++){var p=h[n]=[],q=b[n];if(q[0]!=g.call(q[0]))switch(p[0]=g.call(q[0]),p[0]){case"a":p[1]=q[1],p[2]=q[2],p[3]=q[3],p[4]=q[4],p[5]=q[5],p[6]=+(q[6]-i).toFixed(3),p[7]=+(q[7]-j).toFixed(3);break;case"v":p[1]=+(q[1]-j).toFixed(3);break;case"m":k=q[1],l=q[2];default:for(var r=1,s=q.length;s>r;r++)p[r]=+(q[r]-(r%2?i:j)).toFixed(3)}else{p=h[n]=[],"m"==q[0]&&(k=q[1]+i,l=q[2]+j);for(var t=0,u=q.length;u>t;t++)h[n][t]=q[t]}var v=h[n].length;switch(h[n][0]){case"z":i=k,j=l;break;case"h":i+=+h[n][v-1];break;case"v":j+=+h[n][v-1];break;default:i+=+h[n][v-2],j+=+h[n][v-1]}}return h.toString=e,d.rel=f(h),h}function z(b){var d=c(b);if(d.abs)return f(d.abs);if(I(b,"array")&&I(b&&b[0],"array")||(b=a.parsePathString(b)),!b||!b.length)return[["M",0,0]];var g,h=[],i=0,j=0,k=0,l=0,m=0;"M"==b[0][0]&&(i=+b[0][1],j=+b[0][2],k=i,l=j,m++,h[0]=["M",i,j]);for(var n,o,p=3==b.length&&"M"==b[0][0]&&"R"==b[1][0].toUpperCase()&&"Z"==b[2][0].toUpperCase(),q=m,r=b.length;r>q;q++){if(h.push(n=[]),o=b[q],g=o[0],g!=g.toUpperCase())switch(n[0]=g.toUpperCase(),n[0]){case"A":n[1]=o[1],n[2]=o[2],n[3]=o[3],n[4]=o[4],n[5]=o[5],n[6]=+o[6]+i,n[7]=+o[7]+j;break;case"V":n[1]=+o[1]+j;break;case"H":n[1]=+o[1]+i;break;case"R":for(var s=[i,j].concat(o.slice(1)),t=2,u=s.length;u>t;t++)s[t]=+s[t]+i,s[++t]=+s[t]+j;h.pop(),h=h.concat(G(s,p));break;case"O":h.pop(),s=x(i,j,o[1],o[2]),s.push(s[0]),h=h.concat(s);break;case"U":h.pop(),h=h.concat(x(i,j,o[1],o[2],o[3])),n=["U"].concat(h[h.length-1].slice(-2));break;case"M":k=+o[1]+i,l=+o[2]+j;default:for(t=1,u=o.length;u>t;t++)n[t]=+o[t]+(t%2?i:j)}else if("R"==g)s=[i,j].concat(o.slice(1)),h.pop(),h=h.concat(G(s,p)),n=["R"].concat(o.slice(-2));else if("O"==g)h.pop(),s=x(i,j,o[1],o[2]),s.push(s[0]),h=h.concat(s);else if("U"==g)h.pop(),h=h.concat(x(i,j,o[1],o[2],o[3])),n=["U"].concat(h[h.length-1].slice(-2));else for(var v=0,w=o.length;w>v;v++)n[v]=o[v];if(g=g.toUpperCase(),"O"!=g)switch(n[0]){case"Z":i=+k,j=+l;break;case"H":i=n[1];break;case"V":j=n[1];break;case"M":k=n[n.length-2],l=n[n.length-1];default:i=n[n.length-2],j=n[n.length-1]}}return h.toString=e,d.abs=f(h),h}function A(a,b,c,d){return[a,b,c,d,c,d]}function B(a,b,c,d,e,f){var g=1/3,h=2/3;return[g*a+h*c,g*b+h*d,g*e+h*c,g*f+h*d,e,f]}function C(b,c,d,e,f,g,h,i,j,k){var l,m=120*O/180,n=O/180*(+f||0),o=[],p=a._.cacher(function(a,b,c){var d=a*N.cos(c)-b*N.sin(c),e=a*N.sin(c)+b*N.cos(c);return{x:d,y:e}});if(k)y=k[0],z=k[1],w=k[2],x=k[3];else{l=p(b,c,-n),b=l.x,c=l.y,l=p(i,j,-n),i=l.x,j=l.y;var q=(N.cos(O/180*f),N.sin(O/180*f),(b-i)/2),r=(c-j)/2,s=q*q/(d*d)+r*r/(e*e);s>1&&(s=N.sqrt(s),d=s*d,e=s*e);var t=d*d,u=e*e,v=(g==h?-1:1)*N.sqrt(S((t*u-t*r*r-u*q*q)/(t*r*r+u*q*q))),w=v*d*r/e+(b+i)/2,x=v*-e*q/d+(c+j)/2,y=N.asin(((c-x)/e).toFixed(9)),z=N.asin(((j-x)/e).toFixed(9));y=w>b?O-y:y,z=w>i?O-z:z,0>y&&(y=2*O+y),0>z&&(z=2*O+z),h&&y>z&&(y-=2*O),!h&&z>y&&(z-=2*O)}var A=z-y;if(S(A)>m){var B=z,D=i,E=j;z=y+m*(h&&z>y?1:-1),i=w+d*N.cos(z),j=x+e*N.sin(z),o=C(i,j,d,e,f,0,h,D,E,[z,B,w,x])}A=z-y;var F=N.cos(y),G=N.sin(y),H=N.cos(z),I=N.sin(z),J=N.tan(A/4),K=4/3*d*J,L=4/3*e*J,M=[b,c],P=[b+K*G,c-L*F],Q=[i+K*I,j-L*H],R=[i,j];if(P[0]=2*M[0]-P[0],P[1]=2*M[1]-P[1],k)return[P,Q,R].concat(o);o=[P,Q,R].concat(o).join().split(",");for(var T=[],U=0,V=o.length;V>U;U++)T[U]=U%2?p(o[U-1],o[U],n).y:p(o[U],o[U+1],n).x;return T}function D(a,b,c,d,e,f,g,h){for(var i,j,k,l,m,n,o,p,q=[],r=[[],[]],s=0;2>s;++s)if(0==s?(j=6*a-12*c+6*e,i=-3*a+9*c-9*e+3*g,k=3*c-3*a):(j=6*b-12*d+6*f,i=-3*b+9*d-9*f+3*h,k=3*d-3*b),S(i)<1e-12){if(S(j)<1e-12)continue;l=-k/j,l>0&&1>l&&q.push(l)}else o=j*j-4*k*i,p=N.sqrt(o),0>o||(m=(-j+p)/(2*i),m>0&&1>m&&q.push(m),n=(-j-p)/(2*i),n>0&&1>n&&q.push(n));for(var t,u=q.length,v=u;u--;)l=q[u],t=1-l,r[0][u]=t*t*t*a+3*t*t*l*c+3*t*l*l*e+l*l*l*g,r[1][u]=t*t*t*b+3*t*t*l*d+3*t*l*l*f+l*l*l*h;return r[0][v]=a,r[1][v]=b,r[0][v+1]=g,r[1][v+1]=h,r[0].length=r[1].length=v+2,{min:{x:P.apply(0,r[0]),y:P.apply(0,r[1])},max:{x:Q.apply(0,r[0]),y:Q.apply(0,r[1])}}}function E(a,b){var d=!b&&c(a);if(!b&&d.curve)return f(d.curve);for(var e=z(a),g=b&&z(b),h={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},i={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},j=(function(a,b,c){var d,e;if(!a)return["C",b.x,b.y,b.x,b.y,b.x,b.y];switch(!(a[0]in{T:1,Q:1})&&(b.qx=b.qy=null),a[0]){case"M":b.X=a[1],b.Y=a[2];break;case"A":a=["C"].concat(C.apply(0,[b.x,b.y].concat(a.slice(1))));break;case"S":"C"==c||"S"==c?(d=2*b.x-b.bx,e=2*b.y-b.by):(d=b.x,e=b.y),a=["C",d,e].concat(a.slice(1));break;case"T":"Q"==c||"T"==c?(b.qx=2*b.x-b.qx,b.qy=2*b.y-b.qy):(b.qx=b.x,b.qy=b.y),a=["C"].concat(B(b.x,b.y,b.qx,b.qy,a[1],a[2]));break;case"Q":b.qx=a[1],b.qy=a[2],a=["C"].concat(B(b.x,b.y,a[1],a[2],a[3],a[4]));break;case"L":a=["C"].concat(A(b.x,b.y,a[1],a[2]));break;case"H":a=["C"].concat(A(b.x,b.y,a[1],b.y));break;case"V":a=["C"].concat(A(b.x,b.y,b.x,a[1]));break;case"Z":a=["C"].concat(A(b.x,b.y,b.X,b.Y))}return a}),k=function(a,b){if(a[b].length>7){a[b].shift();for(var c=a[b];c.length;)m[b]="A",g&&(n[b]="A"),a.splice(b++,0,["C"].concat(c.splice(0,6)));a.splice(b,1),r=Q(e.length,g&&g.length||0)}},l=function(a,b,c,d,f){a&&b&&"M"==a[f][0]&&"M"!=b[f][0]&&(b.splice(f,0,["M",d.x,d.y]),c.bx=0,c.by=0,c.x=a[f][1],c.y=a[f][2],r=Q(e.length,g&&g.length||0))},m=[],n=[],o="",p="",q=0,r=Q(e.length,g&&g.length||0);r>q;q++){e[q]&&(o=e[q][0]),"C"!=o&&(m[q]=o,q&&(p=m[q-1])),e[q]=j(e[q],h,p),"A"!=m[q]&&"C"==o&&(m[q]="C"),k(e,q),g&&(g[q]&&(o=g[q][0]),"C"!=o&&(n[q]=o,q&&(p=n[q-1])),g[q]=j(g[q],i,p),"A"!=n[q]&&"C"==o&&(n[q]="C"),k(g,q)),l(e,g,h,i,q),l(g,e,i,h,q);var s=e[q],t=g&&g[q],u=s.length,v=g&&t.length;h.x=s[u-2],h.y=s[u-1],h.bx=M(s[u-4])||h.x,h.by=M(s[u-3])||h.y,i.bx=g&&(M(t[v-4])||i.x),i.by=g&&(M(t[v-3])||i.y),i.x=g&&t[v-2],i.y=g&&t[v-1]}return g||(d.curve=f(e)),g?[e,g]:e}function F(a,b){if(!b)return a;var c,d,e,f,g,h,i;for(a=E(a),e=0,g=a.length;g>e;e++)for(i=a[e],f=1,h=i.length;h>f;f+=2)c=b.x(i[f],i[f+1]),d=b.y(i[f],i[f+1]),i[f]=c,i[f+1]=d;return a}function G(a,b){for(var c=[],d=0,e=a.length;e-2*!b>d;d+=2){var f=[{x:+a[d-2],y:+a[d-1]},{x:+a[d],y:+a[d+1]},{x:+a[d+2],y:+a[d+3]},{x:+a[d+4],y:+a[d+5]}];b?d?e-4==d?f[3]={x:+a[0],y:+a[1]}:e-2==d&&(f[2]={x:+a[0],y:+a[1]},f[3]={x:+a[2],y:+a[3]}):f[0]={x:+a[e-2],y:+a[e-1]}:e-4==d?f[3]=f[2]:d||(f[0]={x:+a[d],y:+a[d+1]}),c.push(["C",(-f[0].x+6*f[1].x+f[2].x)/6,(-f[0].y+6*f[1].y+f[2].y)/6,(f[1].x+6*f[2].x-f[3].x)/6,(f[1].y+6*f[2].y-f[3].y)/6,f[2].x,f[2].y])}return c}var H=b.prototype,I=a.is,J=a._.clone,K="hasOwnProperty",L=/,?([a-z]),?/gi,M=parseFloat,N=Math,O=N.PI,P=N.min,Q=N.max,R=N.pow,S=N.abs,T=h(1),U=h(),V=h(0,1),W=a._unit2px,X={path:function(a){return a.attr("path")},circle:function(a){var b=W(a);return x(b.cx,b.cy,b.r)},ellipse:function(a){var b=W(a);return x(b.cx||0,b.cy||0,b.rx,b.ry)},rect:function(a){var b=W(a);return w(b.x||0,b.y||0,b.width,b.height,b.rx,b.ry)},image:function(a){var b=W(a);return w(b.x||0,b.y||0,b.width,b.height)},line:function(a){return"M"+[a.attr("x1")||0,a.attr("y1")||0,a.attr("x2"),a.attr("y2")]},polyline:function(a){return"M"+a.attr("points")},polygon:function(a){return"M"+a.attr("points")+"z"},deflt:function(a){var b=a.node.getBBox();return w(b.x,b.y,b.width,b.height)}};a.path=c,a.path.getTotalLength=T,a.path.getPointAtLength=U,a.path.getSubpath=function(a,b,c){if(this.getTotalLength(a)-c<1e-6)return V(a,b).end;var d=V(a,c,1);return b?V(d,b).end:d},H.getTotalLength=function(){return this.node.getTotalLength?this.node.getTotalLength():void 0},H.getPointAtLength=function(a){return U(this.attr("d"),a)},H.getSubpath=function(b,c){return a.path.getSubpath(this.attr("d"),b,c)},a._.box=d,a.path.findDotsAtSegment=i,a.path.bezierBBox=j,a.path.isPointInsideBBox=k,a.closest=function(b,c,e,f){for(var g=100,h=d(b-g/2,c-g/2,g,g),i=[],j=e[0].hasOwnProperty("x")?function(a){return{x:e[a].x,y:e[a].y}}:function(a){return{x:e[a],y:f[a]}},l=0;1e6>=g&&!l;){for(var m=0,n=e.length;n>m;m++){var o=j(m);if(k(h,o.x,o.y)){l++,i.push(o);break}}l||(g*=2,h=d(b-g/2,c-g/2,g,g))}if(1e6!=g){var p,q=1/0;for(m=0,n=i.length;n>m;m++){var r=a.len(b,c,i[m].x,i[m].y);q>r&&(q=r,i[m].len=r,p=i[m])}return p}},a.path.isBBoxIntersect=l,a.path.intersection=r,a.path.intersectionNumber=s,a.path.isPointInside=u,a.path.getBBox=v,a.path.get=X,a.path.toRelative=y,a.path.toAbsolute=z,a.path.toCubic=E,a.path.map=F,a.path.toString=e,a.path.clone=f}),d.plugin(function(a){var d=Math.max,e=Math.min,f=function(a){if(this.items=[],this.bindings={},this.length=0,this.type="set",a)for(var b=0,c=a.length;c>b;b++)a[b]&&(this[this.items.length]=this.items[this.items.length]=a[b],this.length++)},g=f.prototype;g.push=function(){for(var a,b,c=0,d=arguments.length;d>c;c++)a=arguments[c],a&&(b=this.items.length,this[b]=this.items[b]=a,this.length++);return this},g.pop=function(){return this.length&&delete this[this.length--],this.items.pop()},g.forEach=function(a,b){for(var c=0,d=this.items.length;d>c;c++)if(a.call(b,this.items[c],c)===!1)return this;return this},g.animate=function(d,e,f,g){"function"!=typeof f||f.length||(g=f,f=c.linear),d instanceof a._.Animation&&(g=d.callback,f=d.easing,e=f.dur,d=d.attr);var h=arguments;if(a.is(d,"array")&&a.is(h[h.length-1],"array"))var i=!0;var j,k=function(){j?this.b=j:j=this.b},l=0,m=this,n=g&&function(){++l==m.length&&g.call(this)
};return this.forEach(function(a,c){b.once("snap.animcreated."+a.id,k),i?h[c]&&a.animate.apply(a,h[c]):a.animate(d,e,f,n)})},g.remove=function(){for(;this.length;)this.pop().remove();return this},g.bind=function(a,b,c){var d={};if("function"==typeof b)this.bindings[a]=b;else{var e=c||a;this.bindings[a]=function(a){d[e]=a,b.attr(d)}}return this},g.attr=function(a){var b={};for(var c in a)this.bindings[c]?this.bindings[c](a[c]):b[c]=a[c];for(var d=0,e=this.items.length;e>d;d++)this.items[d].attr(b);return this},g.clear=function(){for(;this.length;)this.pop()},g.splice=function(a,b){a=0>a?d(this.length+a,0):a,b=d(0,e(this.length-a,b));var c,g=[],h=[],i=[];for(c=2;cc;c++)h.push(this[a+c]);for(;cc?i[c]:g[c-j];for(c=this.items.length=this.length-=b-j;this[c];)delete this[c++];return new f(h)},g.exclude=function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]==a)return this.splice(b,1),!0;return!1},g.insertAfter=function(a){for(var b=this.items.length;b--;)this.items[b].insertAfter(a);return this},g.getBBox=function(){for(var a=[],b=[],c=[],f=[],g=this.items.length;g--;)if(!this.items[g].removed){var h=this.items[g].getBBox();a.push(h.x),b.push(h.y),c.push(h.x+h.width),f.push(h.y+h.height)}return a=e.apply(0,a),b=e.apply(0,b),c=d.apply(0,c),f=d.apply(0,f),{x:a,y:b,x2:c,y2:f,width:c-a,height:f-b,cx:a+(c-a)/2,cy:b+(f-b)/2}},g.clone=function(a){a=new f;for(var b=0,c=this.items.length;c>b;b++)a.push(this.items[b].clone());return a},g.toString=function(){return"Snap‘s set"},g.type="set",a.Set=f,a.set=function(){var a=new f;return arguments.length&&a.push.apply(a,Array.prototype.slice.call(arguments,0)),a}}),d.plugin(function(a,c){function d(a){var b=a[0];switch(b.toLowerCase()){case"t":return[b,0,0];case"m":return[b,1,0,0,1,0,0];case"r":return 4==a.length?[b,0,a[2],a[3]]:[b,0];case"s":return 5==a.length?[b,1,1,a[3],a[4]]:3==a.length?[b,1,1]:[b,1]}}function e(b,c,e){c=p(c).replace(/\.{3}|\u2026/g,b),b=a.parseTransformString(b)||[],c=a.parseTransformString(c)||[];for(var f,g,h,i,l=Math.max(b.length,c.length),m=[],n=[],o=0;l>o;o++){if(h=b[o]||d(c[o]),i=c[o]||d(h),h[0]!=i[0]||"r"==h[0].toLowerCase()&&(h[2]!=i[2]||h[3]!=i[3])||"s"==h[0].toLowerCase()&&(h[3]!=i[3]||h[4]!=i[4])){b=a._.transform2matrix(b,e()),c=a._.transform2matrix(c,e()),m=[["m",b.a,b.b,b.c,b.d,b.e,b.f]],n=[["m",c.a,c.b,c.c,c.d,c.e,c.f]];break}for(m[o]=[],n[o]=[],f=0,g=Math.max(h.length,i.length);g>f;f++)f in h&&(m[o][f]=h[f]),f in i&&(n[o][f]=i[f])}return{from:k(m),to:k(n),f:j(m)}}function f(a){return a}function g(a){return function(b){return+b.toFixed(3)+a}}function h(a){return a.join(" ")}function i(b){return a.rgb(b[0],b[1],b[2])}function j(a){var b,c,d,e,f,g,h=0,i=[];for(b=0,c=a.length;c>b;b++){for(f="[",g=['"'+a[b][0]+'"'],d=1,e=a[b].length;e>d;d++)g[d]="val["+h++ +"]";f+=g+"]",i[b]=f}return Function("val","return Snap.path.toString.call(["+i+"])")}function k(a){for(var b=[],c=0,d=a.length;d>c;c++)for(var e=1,f=a[c].length;f>e;e++)b.push(a[c][e]);return b}function l(a){return isFinite(parseFloat(a))}function m(b,c){return a.is(b,"array")&&a.is(c,"array")?b.toString()==c.toString():!1}var n={},o=/[a-z]+$/i,p=String;n.stroke=n.fill="colour",c.prototype.equal=function(a,c){return b("snap.util.equal",this,a,c).firstDefined()},b.on("snap.util.equal",function(b,c){var d,q,r=p(this.attr(b)||""),s=this;if(l(r)&&l(c))return{from:parseFloat(r),to:parseFloat(c),f:f};if("colour"==n[b])return d=a.color(r),q=a.color(c),{from:[d.r,d.g,d.b,d.opacity],to:[q.r,q.g,q.b,q.opacity],f:i};if("viewBox"==b)return d=this.attr(b).vb.split(" ").map(Number),q=c.split(" ").map(Number),{from:d,to:q,f:h};if("transform"==b||"gradientTransform"==b||"patternTransform"==b)return c instanceof a.Matrix&&(c=c.toTransformString()),a._.rgTransform.test(c)||(c=a._.svgTransform2string(c)),e(r,c,function(){return s.getBBox(1)});if("d"==b||"path"==b)return d=a.path.toCubic(r,c),{from:k(d[0]),to:k(d[1]),f:j(d[0])};if("points"==b)return d=p(r).split(a._.separator),q=p(c).split(a._.separator),{from:d,to:q,f:function(a){return a}};var t=r.match(o),u=p(c).match(o);return t&&m(t,u)?{from:parseFloat(r),to:parseFloat(c),f:g(t)}:{from:this.asPX(b),to:this.asPX(b,c),f:f}})}),d.plugin(function(a,c,d,e){for(var f=c.prototype,g="hasOwnProperty",h=("createTouch"in e.doc),i=["click","dblclick","mousedown","mousemove","mouseout","mouseover","mouseup","touchstart","touchmove","touchend","touchcancel"],j={mousedown:"touchstart",mousemove:"touchmove",mouseup:"touchend"},k=(function(a,b){var c="y"==a?"scrollTop":"scrollLeft",d=b&&b.node?b.node.ownerDocument:e.doc;return d[c in d.documentElement?"documentElement":"body"][c]}),l=function(){return this.originalEvent.preventDefault()},m=function(){return this.originalEvent.stopPropagation()},n=function(a,b,c,d){var e=h&&j[b]?j[b]:b,f=function(e){var f=k("y",d),i=k("x",d);if(h&&j[g](b))for(var n=0,o=e.targetTouches&&e.targetTouches.length;o>n;n++)if(e.targetTouches[n].target==a||a.contains(e.targetTouches[n].target)){var p=e;e=e.targetTouches[n],e.originalEvent=p,e.preventDefault=l,e.stopPropagation=m;break}var q=e.clientX+i,r=e.clientY+f;return c.call(d,e,q,r)};return b!==e&&a.addEventListener(b,f,!1),a.addEventListener(e,f,!1),function(){return b!==e&&a.removeEventListener(b,f,!1),a.removeEventListener(e,f,!1),!0}},o=[],p=function(a){for(var c,d=a.clientX,e=a.clientY,f=k("y"),g=k("x"),i=o.length;i--;){if(c=o[i],h){for(var j,l=a.touches&&a.touches.length;l--;)if(j=a.touches[l],j.identifier==c.el._drag.id||c.el.node.contains(j.target)){d=j.clientX,e=j.clientY,(a.originalEvent?a.originalEvent:a).preventDefault();break}}else a.preventDefault();{var m=c.el.node;m.nextSibling,m.parentNode,m.style.display}d+=g,e+=f,b("snap.drag.move."+c.el.id,c.move_scope||c.el,d-c.el._drag.x,e-c.el._drag.y,d,e,a)}},q=function(c){a.unmousemove(p).unmouseup(q);for(var d,e=o.length;e--;)d=o[e],d.el._drag={},b("snap.drag.end."+d.el.id,d.end_scope||d.start_scope||d.move_scope||d.el,c),b.off("snap.drag.*."+d.el.id);o=[]},r=i.length;r--;)!function(b){a[b]=f[b]=function(c,d){if(a.is(c,"function"))this.events=this.events||[],this.events.push({name:b,f:c,unbind:n(this.node||document,b,c,d||this)});else for(var e=0,f=this.events.length;f>e;e++)if(this.events[e].name==b)try{this.events[e].f.call(this)}catch(g){}return this},a["un"+b]=f["un"+b]=function(a){for(var c=this.events||[],d=c.length;d--;)if(c[d].name==b&&(c[d].f==a||!a))return c[d].unbind(),c.splice(d,1),!c.length&&delete this.events,this;return this}}(i[r]);f.hover=function(a,b,c,d){return this.mouseover(a,c).mouseout(b,d||c)},f.unhover=function(a,b){return this.unmouseover(a).unmouseout(b)};var s=[];f.drag=function(c,d,e,f,g,h){function i(i,j,l){(i.originalEvent||i).preventDefault(),k._drag.x=j,k._drag.y=l,k._drag.id=i.identifier,!o.length&&a.mousemove(p).mouseup(q),o.push({el:k,move_scope:f,start_scope:g,end_scope:h}),d&&b.on("snap.drag.start."+k.id,d),c&&b.on("snap.drag.move."+k.id,c),e&&b.on("snap.drag.end."+k.id,e),b("snap.drag.start."+k.id,g||f||k,j,l,i)}function j(a,c,d){b("snap.draginit."+k.id,k,a,c,d)}var k=this;if(!arguments.length){var l;return k.drag(function(a,b){this.attr({transform:l+(l?"T":"t")+[a,b]})},function(){l=this.transform().local})}return b.on("snap.draginit."+k.id,i),k._drag={},s.push({el:k,start:i,init:j}),k.mousedown(j),k},f.undrag=function(){for(var c=s.length;c--;)s[c].el==this&&(this.unmousedown(s[c].init),s.splice(c,1),b.unbind("snap.drag.*."+this.id),b.unbind("snap.draginit."+this.id));return!s.length&&a.unmousemove(p).unmouseup(q),this}}),d.plugin(function(a,c,d){var e=(c.prototype,d.prototype),f=/^\s*url\((.+)\)/,g=String,h=a._.$;a.filter={},e.filter=function(b){var d=this;"svg"!=d.type&&(d=d.paper);var e=a.parse(g(b)),f=a._.id(),i=(d.node.offsetWidth,d.node.offsetHeight,h("filter"));return h(i,{id:f,filterUnits:"userSpaceOnUse"}),i.appendChild(e.node),d.defs.appendChild(i),new c(i)},b.on("snap.util.getattr.filter",function(){b.stop();var c=h(this.node,"filter");if(c){var d=g(c).match(f);return d&&a.select(d[1])}}),b.on("snap.util.attr.filter",function(d){if(d instanceof c&&"filter"==d.type){b.stop();var e=d.node.id;e||(h(d.node,{id:d.id}),e=d.id),h(this.node,{filter:a.url(e)})}d&&"none"!=d||(b.stop(),this.node.removeAttribute("filter"))}),a.filter.blur=function(b,c){null==b&&(b=2);var d=null==c?b:[b,c];return a.format('',{def:d})},a.filter.blur.toString=function(){return this()},a.filter.shadow=function(b,c,d,e,f){return"string"==typeof d&&(e=d,f=e,d=4),"string"!=typeof e&&(f=e,e="#000"),e=e||"#000",null==d&&(d=4),null==f&&(f=1),null==b&&(b=0,c=2),null==c&&(c=b),e=a.color(e),a.format('',{color:e,dx:b,dy:c,blur:d,opacity:f})},a.filter.shadow.toString=function(){return this()},a.filter.grayscale=function(b){return null==b&&(b=1),a.format('',{a:.2126+.7874*(1-b),b:.7152-.7152*(1-b),c:.0722-.0722*(1-b),d:.2126-.2126*(1-b),e:.7152+.2848*(1-b),f:.0722-.0722*(1-b),g:.2126-.2126*(1-b),h:.0722+.9278*(1-b)})},a.filter.grayscale.toString=function(){return this()},a.filter.sepia=function(b){return null==b&&(b=1),a.format('',{a:.393+.607*(1-b),b:.769-.769*(1-b),c:.189-.189*(1-b),d:.349-.349*(1-b),e:.686+.314*(1-b),f:.168-.168*(1-b),g:.272-.272*(1-b),h:.534-.534*(1-b),i:.131+.869*(1-b)})},a.filter.sepia.toString=function(){return this()},a.filter.saturate=function(b){return null==b&&(b=1),a.format('',{amount:1-b})},a.filter.saturate.toString=function(){return this()},a.filter.hueRotate=function(b){return b=b||0,a.format('',{angle:b})},a.filter.hueRotate.toString=function(){return this()},a.filter.invert=function(b){return null==b&&(b=1),a.format('',{amount:b,amount2:1-b})},a.filter.invert.toString=function(){return this()},a.filter.brightness=function(b){return null==b&&(b=1),a.format('',{amount:b})},a.filter.brightness.toString=function(){return this()},a.filter.contrast=function(b){return null==b&&(b=1),a.format('',{amount:b,amount2:.5-b/2})},a.filter.contrast.toString=function(){return this()}}),d.plugin(function(a,b){var c=a._.box,d=a.is,e=/^[^a-z]*([tbmlrc])/i,f=function(){return"T"+this.dx+","+this.dy};b.prototype.getAlign=function(a,b){null==b&&d(a,"string")&&(b=a,a=null),a=a||this.paper;var g=a.getBBox?a.getBBox():c(a),h=this.getBBox(),i={};switch(b=b&&b.match(e),b=b?b[1].toLowerCase():"c"){case"t":i.dx=0,i.dy=g.y-h.y;break;case"b":i.dx=0,i.dy=g.y2-h.y2;break;case"m":i.dx=0,i.dy=g.cy-h.cy;break;case"l":i.dx=g.x-h.x,i.dy=0;break;case"r":i.dx=g.x2-h.x2,i.dy=0;break;default:i.dx=g.cx-h.cx,i.dy=0}return i.toString=f,i},b.prototype.align=function(a,b){return this.transform("..."+this.getAlign(a,b))}}),d});
diff --git a/src/octoprint/plugins/svgtogcode/static/js/matrix_oven.js b/src/octoprint/plugins/svgtogcode/static/js/matrix_oven.js
index 80548c50..05b4ffb7 100644
--- a/src/octoprint/plugins/svgtogcode/static/js/matrix_oven.js
+++ b/src/octoprint/plugins/svgtogcode/static/js/matrix_oven.js
@@ -398,9 +398,13 @@ Snap.plugin(function (Snap, Element, Paper, global) {
var d = '';
- var valid = function (val) {
+ var validRadius = function (val) {
return (isFinite(val) && (val >= 0));
};
+
+ var validCoordinate = function (val) {
+ return (isFinite(val));
+ };
// Possibly the cubed root of 6, but 1.81 works best
var num = 1.81;
@@ -415,7 +419,13 @@ Snap.plugin(function (Snap, Element, Paper, global) {
if (tag === 'circle') {
rx = ry = +old_element.attr('r');
}
-
+
+ // If 'x' and 'y' are not specified, then set both to 0. // CorelDraw is creating that sometimes
+ if (!validCoordinate(cx))
+ cx = 0;
+ if (!validCoordinate(cy))
+ cy = 0;
+
d += _convertToString([
['M', (cx - rx), (cy)],
['C', (cx - rx), (cy - ry / num), (cx - rx / num), (cy - ry), (cx), (cy - ry)],
@@ -451,14 +461,19 @@ Snap.plugin(function (Snap, Element, Paper, global) {
h = parseFloat(old_element.attr('height'));
// Validity checks from http://www.w3.org/TR/SVG/shapes.html#RectElement:
+ // If 'x' and 'y' are not specified, then set both to 0. // CorelDraw is creating that sometimes
+ if (!validCoordinate(x))
+ x = 0;
+ if (!validCoordinate(y))
+ y = 0;
// If neither ‘rx’ nor ‘ry’ are properly specified, then set both rx and ry to 0. (This will result in square corners.)
- if (!valid(rx) && !valid(ry)) {
+ if (!validRadius(rx) && !validRadius(ry)) {
rx = ry = 0;
// Otherwise, if a properly specified value is provided for ‘rx’, but not for ‘ry’, then set both rx and ry to the value of ‘rx’.
- } else if (valid(rx) && !valid(ry)) {
+ } else if (validRadius(rx) && !validRadius(ry)) {
ry = rx;
// Otherwise, if a properly specified value is provided for ‘ry’, but not for ‘rx’, then set both rx and ry to the value of ‘ry’.
- } else if (valid(ry) && !valid(rx)) {
+ } else if (validRadius(ry) && !validRadius(rx)) {
rx = ry;
} else { // cap values for rx/ry to half of w/h
rx = Math.min(rx, w/2);
diff --git a/src/octoprint/plugins/svgtogcode/static/js/render_fills.js b/src/octoprint/plugins/svgtogcode/static/js/render_fills.js
index 60cc0d4e..60b55bd6 100644
--- a/src/octoprint/plugins/svgtogcode/static/js/render_fills.js
+++ b/src/octoprint/plugins/svgtogcode/static/js/render_fills.js
@@ -29,7 +29,7 @@ Snap.plugin(function (Snap, Element, Paper, global) {
* @returns {path}
*/
- Element.prototype.removeUnfilled = function(){
+ Element.prototype.removeUnfilled = function(fillPaths){
var elem = this;
var selection = [];
var children = elem.children();
@@ -46,14 +46,18 @@ Snap.plugin(function (Snap, Element, Paper, global) {
if(goRecursive) {
for (var i = 0; i < children.length; i++) {
var child = children[i];
- selection = selection.concat(child.removeUnfilled());
+ selection = selection.concat(child.removeUnfilled(fillPaths));
}
}
} else {
- if(elem.is_filled()){
+ if(elem.type === 'image'){
selection.push(elem);
} else {
- elem.remove();
+ if(fillPaths && elem.is_filled()){
+ selection.push(elem);
+ } else {
+ elem.remove();
+ }
}
}
return selection;
@@ -70,16 +74,11 @@ Snap.plugin(function (Snap, Element, Paper, global) {
elem.type !== "line" &&
elem.type !== "polygon" &&
elem.type !== "polyline" &&
- elem.type !== "path" &&
- elem.type !== "image"){
+ elem.type !== "path" ){
return false;
}
- if(elem.type === 'image'){
- return true;
- }
-
var fill = elem.attr('fill');
var opacity = elem.attr('fill-opacity');
diff --git a/src/octoprint/plugins/svgtogcode/static/js/working_area.js b/src/octoprint/plugins/svgtogcode/static/js/working_area.js
index 2ae60f1b..85b57bdf 100644
--- a/src/octoprint/plugins/svgtogcode/static/js/working_area.js
+++ b/src/octoprint/plugins/svgtogcode/static/js/working_area.js
@@ -109,6 +109,8 @@ $(function(){
if(self.state.isOperational() && !self.state.isPrinting()){
var x = self.px2mm(event.offsetX);
var y = self.px2mm(event.toElement.ownerSVGElement.offsetHeight - event.offsetY); // hopefully this works across browsers
+ x = Math.min(x, self.workingAreaWidthMM());
+ y = Math.min(y, self.workingAreaHeightMM());
$.ajax({
url: API_BASEURL + "printer/printhead",
type: "POST",
@@ -335,7 +337,7 @@ $(function(){
var tooHigh = svgBB.h > waBB.h;
var scale = 1;
if(tooWide || tooHigh){
- scale = Math.min(waBB.w / svgBB.w, waBB.h / svgBB.h) - 0.01; // scale minimal smaller to avoid rounding errors
+ scale = Math.min(waBB.w / svgBB.w, waBB.h / svgBB.h) - 0.0001; // scale minimal smaller to avoid rounding errors
}
var dx = 0;
@@ -623,7 +625,7 @@ $(function(){
}
};
- self.getCompositionSVG = function(callback){
+ self.getCompositionSVG = function(fillAreas, callback){
self.abortFreeTransforms();
var wMM = self.workingAreaWidthMM();
var hMM = self.workingAreaHeightMM();
@@ -635,8 +637,8 @@ $(function(){
var userContent = snap.select("#userContent").clone();
compSvg.append(userContent);
- self.renderInfill(compSvg, wMM, hMM, 10, function(){
- callback( self._wrapInSvgAndScale(compSvg));
+ self.renderInfill(compSvg, fillAreas, wMM, hMM, 10, function(svgWithRenderedInfill){
+ callback( self._wrapInSvgAndScale(svgWithRenderedInfill));
$('#compSvg').remove();
});
};
@@ -684,6 +686,19 @@ $(function(){
return gcodeFiles;
}, self);
+ self.hasFilledVectors = function(){
+ var el = snap.selectAll('#userContent *');
+ for (var i = 0; i < el.length; i++) {
+ var e = el[i];
+ var fill = e.attr('fill');
+ var op = e.attr('fill-opacity');
+ if(fill !== 'none' && op > 0){
+ return true;
+ }
+
+ }
+ return false;
+ };
self.draw_gcode = function(points, intensity, target){
var stroke_color = intensity === 0 ? '#BBBBBB' : '#FF0000';
@@ -697,12 +712,6 @@ $(function(){
};
self.draw_gcode_img_placeholder = function(x,y,w,h,url, target){
- var i = snap.rect(x,y,w,h).attr({
- stroke: '#AAAAAA',
- 'stroke-width': 1,
- fill: 'none'
- });
- snap.select(target).append(i);
if(url !== ""){
var p = snap.image(url,x,y,w,h).attr({
transform: 'matrix(1,0,0,-1,0,'+ String(h) +')',
@@ -774,7 +783,7 @@ $(function(){
}
// render the infill and inject it as an image into the svg
- self.renderInfill = function (svg, wMM, hMM, pxPerMM, callback) {
+ self.renderInfill = function (svg, fillAreas, wMM, hMM, pxPerMM, callback) {
var wPT = wMM * 90 / 25.4;
var hPT = hMM * 90 / 25.4;
var tmpSvg = Snap(wPT, hPT).attr('id', 'tmpSvg');
@@ -782,16 +791,19 @@ $(function(){
var userContent = svg.clone();
tmpSvg.append(userContent);
self._embedAllImages(tmpSvg, function(){
- var fillings = userContent.removeUnfilled();
+ var fillings = userContent.removeUnfilled(fillAreas);
for (var i = 0; i < fillings.length; i++) {
var item = fillings[i];
+
if (item.type === 'image') {
+ // remove filter effects on images for proper rendering
var style = item.attr('style');
if (style !== null) {
var strippedFilters = style.replace(/filter.+?;/, '');
item.attr('style', strippedFilters);
}
} else {
+ // remove stroke from other elements
//item.attr('fill', '#ff0000');
item.attr('stroke', 'none');
}
@@ -799,6 +811,7 @@ $(function(){
var cb = function(result) {
if(fillings.length > 0){
+ // replace all images with the fill rendering
svg.selectAll('image').remove();
var waBB = snap.select('#coordGrid').getBBox();
var fillImage = snap.image(result, 0, 0, waBB.w, waBB.h);
diff --git a/src/octoprint/plugins/svgtogcode/templates/svgtogcode.jinja2 b/src/octoprint/plugins/svgtogcode/templates/svgtogcode.jinja2
index ee46cc96..0eba6848 100644
--- a/src/octoprint/plugins/svgtogcode/templates/svgtogcode.jinja2
+++ b/src/octoprint/plugins/svgtogcode/templates/svgtogcode.jinja2
@@ -38,6 +38,12 @@
The effect in general is dependent from the material and its color and surface.
+
+
+
+
@@ -176,7 +182,7 @@
- {{ _('Cancel') }}
+ {{ _('Cancel') }}
{{ _('Convert') }}
\ No newline at end of file
diff --git a/src/octoprint/printer/__init__.py b/src/octoprint/printer/__init__.py
index b518cb20..2a4446db 100644
--- a/src/octoprint/printer/__init__.py
+++ b/src/octoprint/printer/__init__.py
@@ -23,7 +23,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
import re
-import octoprint.util.comm_acc as comm
+import octoprint.util.comm_acc2 as comm
import octoprint.util as util
from octoprint.settings import settings
diff --git a/src/octoprint/printer/standard.py b/src/octoprint/printer/standard.py
index b0827966..5fee8558 100644
--- a/src/octoprint/printer/standard.py
+++ b/src/octoprint/printer/standard.py
@@ -22,7 +22,7 @@ from octoprint.plugin import plugin_manager, ProgressPlugin
from octoprint.printer import PrinterInterface, PrinterCallback, UnknownScript
from octoprint.printer.estimation import TimeEstimationHelper
from octoprint.settings import settings
-from octoprint.util import comm_acc as comm
+from octoprint.util import comm_acc2 as comm
from octoprint.util import InvariantContainer
diff --git a/src/octoprint/static/js/app/viewmodels/control.js b/src/octoprint/static/js/app/viewmodels/control.js
index 71ecc7b4..cf3b18bf 100644
--- a/src/octoprint/static/js/app/viewmodels/control.js
+++ b/src/octoprint/static/js/app/viewmodels/control.js
@@ -108,11 +108,11 @@ $(function() {
});
}
};
-
+
self.rerenderControls = function () {
var allControls = self.controlsFromServer.concat(self.additionalControls);
- self.controls(self._processControls(allControls))
+ self.controls(self._processControls(allControls));
};
self.requestData = function () {
diff --git a/src/octoprint/static/js/app/viewmodels/files.js b/src/octoprint/static/js/app/viewmodels/files.js
index 57ab14ec..b4f71d85 100644
--- a/src/octoprint/static/js/app/viewmodels/files.js
+++ b/src/octoprint/static/js/app/viewmodels/files.js
@@ -100,7 +100,7 @@ $(function() {
});
}
};
-
+
self.fromCurrentData = function(data) {
self._processStateData(data.state);
};
@@ -138,7 +138,7 @@ $(function() {
}
});
};
-
+
self.fromResponse = function(response, filenameToFocus, locationToFocus) {
var files = response.files;
_.each(files, function(element, index, list) {
@@ -267,7 +267,7 @@ $(function() {
return "files_template_dummy";
}
};
-
+
self.getEntryId = function(data) {
return "gcode_file_" + md5(data["origin"] + ":" + data["name"]);
};
@@ -344,7 +344,7 @@ $(function() {
}
return output;
};
-
+
self.performSearch = function(e) {
if (e !== undefined) {
e.preventDefault();
@@ -375,7 +375,7 @@ $(function() {
self.enableSVGConversion = function (data) {
return self.loginState.isUser() && !(self.isPrinting() || self.isPaused());
};
-
+
self.onStartup = function() {
$(".accordion-toggle[data-target='#files']").click(function() {
var files = $("#files");
@@ -610,11 +610,13 @@ $(function() {
$('#take_photo_dialog').on('shown', function () {
$('#photo_preview').photobooth();
- var w = $('#photo_preview').width();
- var h = $('#photo_preview').height();
+ var w = $('#photo_preview').parent().width()*0.98;
+ var h = w*3.0/4.0;
+ $('#photo_preview').height(h);
+ $('#photo_preview').width(w);
$('#photo_preview').data('photobooth').resize(w, h);
});
-
+
$('#photo_preview').on("image", function (event, dataUrl) {
var photoBlob = self.dataUriToBlob(dataUrl);
var t = new Date();
@@ -660,7 +662,7 @@ $(function() {
);
return !!fGetUserMedia;
};
-
+
self.dataUriToBlob = function(dataURI) {
// serialize the base64/URLEncoded data
var byteString;
diff --git a/src/octoprint/static/js/app/viewmodels/printerstate.js b/src/octoprint/static/js/app/viewmodels/printerstate.js
index 1ceacb01..b6b8f6d5 100644
--- a/src/octoprint/static/js/app/viewmodels/printerstate.js
+++ b/src/octoprint/static/js/app/viewmodels/printerstate.js
@@ -34,6 +34,10 @@ $(function() {
self.currentHeight = ko.observable(undefined);
self.currentPos = ko.observable(undefined);
+ self.intensityOverride = ko.observable(100);
+ self.feedrateOverride = ko.observable(100);
+ self.intensityOverride.extend({ rateLimit: 500 });
+ self.feedrateOverride.extend({ rateLimit: 500 });
self.TITLE_PRINT_BUTTON_PAUSED = gettext("Restarts the print job from the beginning");
self.TITLE_PRINT_BUTTON_UNPAUSED = gettext("Starts the print job");
@@ -281,6 +285,62 @@ $(function() {
self.onEventRealTimeState = function(payload){
self.currentPos({x: payload.wx, y: payload.wy});
};
+
+ self.intensityOverride.subscribe(function(factor){
+ self._overrideCommand("/intensity "+factor);
+ });
+ self.feedrateOverride.subscribe(function(factor){
+ self._overrideCommand("/feedrate "+factor);
+ });
+
+ self._overrideCommand = function(command, callback) {
+ $.ajax({
+ url: API_BASEURL + "printer/command",
+ type: "POST",
+ dataType: "json",
+ contentType: "application/json; charset=UTF-8",
+ data: JSON.stringify({command: command}),
+ success: function(response) {
+ if (callback != undefined) {
+ callback();
+ }
+ }
+ });
+ };
+
+ self._configureOverrideSliders = function() {
+ self.intensityOverrideSlider = $("#intensity_override_slider").slider({
+ step: 1,
+ min: 10,
+ max: 200,
+ value: 100,
+// tooltip: 'hide'
+ }).on("slideStop", function(ev){
+ self.intensityOverride(ev.value);
+ });
+
+ self.feedrateOverrideSlider = $("#feedrate_override_slider").slider({
+ step: 1,
+ min: 10,
+ max: 200,
+ value: 100,
+// tooltip: 'hide'
+ }).on("slideStop", function(ev){
+ self.feedrateOverride(ev.value);
+ });
+
+ };
+
+ self.onEventPrintDone = function(){
+ self.feedrateOverrideSlider.slider('setValue', 100);
+ self.intensityOverrideSlider.slider('setValue', 100);
+ self.intensityOverride(100);
+ self.feedrateOverride(100);
+ };
+
+ self.onStartup = function() {
+ self._configureOverrideSliders();
+ };
}
OCTOPRINT_VIEWMODELS.push([
diff --git a/src/octoprint/templates/mrbeam_index.jinja2 b/src/octoprint/templates/mrbeam_index.jinja2
index 590a3132..46cb87d8 100644
--- a/src/octoprint/templates/mrbeam_index.jinja2
+++ b/src/octoprint/templates/mrbeam_index.jinja2
@@ -155,8 +155,20 @@
{{ _('Timelapse') }}:
-->
{{ _('Approx. Total Job Time') }}:
-
-
{{ _('Processed') }} :
+
+
+
+
{{ _('Processed') }} :
+
+
+
+ 100% Intensity
+
+
+
+ 100% Feedrate
+
+
@@ -270,69 +282,70 @@
-
- {% if webcamStream %}
-
-
-
![]()
-
-
- {% endif %}
-
-
+
+ {% if webcamStream %}
+
+
+
![]()
+
+
+ {% endif %}
+
+
diff --git a/src/octoprint/util/comm_acc.py b/src/octoprint/util/comm_acc.py
index 83d6b82f..8dacbdd5 100644
--- a/src/octoprint/util/comm_acc.py
+++ b/src/octoprint/util/comm_acc.py
@@ -1888,22 +1888,34 @@ class MachineCom(object):
self._doSendWithoutChecksum(commandToSend)
def _doSendWithoutChecksum(self, cmd):
- self._log("Send: %s" % cmd)
- self.acc_line_lengths.append(len(cmd)+1) # Track number of characters in grbl serial read buffer
- try:
- self._serial.write(cmd + '\n')
- except serial.SerialTimeoutException:
- self._log("Serial timeout while writing to serial port, trying again.")
+ if cmd == "?":
+ try:
+ self._serial.write(cmd)
+ except serial.SerialTimeoutException:
+ self._log("Serial timeout while writing to serial port, trying again.")
+ try:
+ self._serial.write(cmd)
+ except:
+ self._log("Unexpected error while writing serial port: %s" % (get_exception_string()))
+ self._errorValue = get_exception_string()
+ self.close(True)
+ else:
+ self._log("Send: %s" % cmd)
+ self.acc_line_lengths.append(len(cmd)+1) # Track number of characters in grbl serial read buffer
try:
self._serial.write(cmd + '\n')
+ except serial.SerialTimeoutException:
+ self._log("Serial timeout while writing to serial port, trying again.")
+ try:
+ self._serial.write(cmd + '\n')
+ except:
+ self._log("Unexpected error while writing serial port: %s" % (get_exception_string()))
+ self._errorValue = get_exception_string()
+ self.close(True)
except:
self._log("Unexpected error while writing serial port: %s" % (get_exception_string()))
self._errorValue = get_exception_string()
self.close(True)
- except:
- self._log("Unexpected error while writing serial port: %s" % (get_exception_string()))
- self._errorValue = get_exception_string()
- self.close(True)
##~~ command handlers
def _gcode_H_sent(self, cmd, cmd_type=None):
diff --git a/src/octoprint/util/comm_acc2.py b/src/octoprint/util/comm_acc2.py
new file mode 100644
index 00000000..39a0318a
--- /dev/null
+++ b/src/octoprint/util/comm_acc2.py
@@ -0,0 +1,1270 @@
+# coding=utf-8
+from __future__ import absolute_import
+__author__ = "Florian Becker based on work by Gina Häußge and David Braam"
+__license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html"
+__copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
+
+import os
+import threading
+import logging
+import glob
+import time
+import serial
+import re
+import Queue
+
+from yaml import load as yamlload
+from yaml import dump as yamldump
+from subprocess import call as subprocesscall
+
+import octoprint.plugin
+
+from octoprint.settings import settings, default_settings
+from octoprint.events import eventManager, Events
+from octoprint.filemanager.destinations import FileDestinations
+from octoprint.util import get_exception_string, RepeatedTimer, CountedEvent, sanitize_ascii
+
+### MachineCom #########################################################################################################
+class MachineCom(object):
+ STATE_NONE = 0
+ STATE_OPEN_SERIAL = 1
+ STATE_DETECT_SERIAL = 2
+ STATE_CONNECTING = 3
+ STATE_OPERATIONAL = 4
+ STATE_PRINTING = 5
+ STATE_PAUSED = 6
+ STATE_CLOSED = 7
+ STATE_ERROR = 8
+ STATE_CLOSED_WITH_ERROR = 9
+ STATE_LOCKED = 10
+ STATE_HOMING = 11
+ STATE_FLASHING = 12
+
+ def __init__(self, port=None, baudrate=None, callbackObject=None, printerProfileManager=None):
+ self._logger = logging.getLogger(__name__)
+ self._serialLogger = logging.getLogger("SERIAL")
+
+ if port is None:
+ port = settings().get(["serial", "port"])
+ elif isinstance(port, list):
+ port = port[0]
+ if baudrate is None:
+ settingsBaudrate = settings().getInt(["serial", "baudrate"])
+ if settingsBaudrate is None:
+ baudrate = 0
+ else:
+ baudrate = settingsBaudrate
+ if callbackObject is None:
+ callbackObject = MachineComPrintCallback()
+
+ self._port = port
+ self._baudrate = baudrate
+ self._callback = callbackObject
+ self._printerProfileManager = printerProfileManager
+
+ self.RX_BUFFER_SIZE = 127
+
+ self._state = self.STATE_NONE
+ self._grbl_state = None
+ self._errorValue = "Unknown Error"
+ self._serial = None
+ self._currentFile = None
+ self._status_timer = None
+ self._acc_line_buffer = []
+ self._cmd = None
+ self._pauseWaitStartTime = None
+ self._pauseWaitTimeLost = 0.0
+ self._commandQueue = Queue.Queue()
+ self._send_event = CountedEvent(max=50)
+ self._finished_currentFile = False
+ self._pause_delay_time = 0
+ self._feedrate_factor = 1
+ self._actual_feedrate = None
+ self._intensity_factor = 1
+ self._actual_intensity = None
+ self._feedrate_dict = {}
+ self._intensity_dict = {}
+
+ # regular expressions
+ self._regex_command = re.compile("^\s*\$?([GM]\d+|[TH])")
+ self._regex_feedrate = re.compile("F\d+", re.IGNORECASE)
+ self._regex_intensity = re.compile("S\d+", re.IGNORECASE)
+
+ self._real_time_commands={'poll_status':False,
+ 'feed_hold':False,
+ 'cycle_start':False,
+ 'soft_reset':False}
+
+ # metric stuff
+ #self._metric_chars = 0
+ #self._metric_time = None
+ #self._metric_active = True
+ #self._metric_thread = threading.Thread(target=self._metric_loop, name="comm._metric_thread")
+ #self._metric_thread.daemon = True
+ #self._metric_thread.start()
+
+ # hooks
+ self._pluginManager = octoprint.plugin.plugin_manager()
+ self._serial_factory_hooks = self._pluginManager.get_hooks("octoprint.comm.transport.serial.factory")
+
+ # monitoring thread
+ self._monitoring_active = True
+ self.monitoring_thread = threading.Thread(target=self._monitor_loop, name="comm._monitoring_thread")
+ self.monitoring_thread.daemon = True
+ self.monitoring_thread.start()
+
+ # sending thread
+ self._sending_active = True
+ self.sending_thread = threading.Thread(target=self._send_loop, name="comm.sending_thread")
+ self.sending_thread.daemon = True
+
+ def _monitor_loop(self):
+ #Open the serial port.
+ if not self._openSerial():
+ return
+
+ self._log("Connected to: %s, starting monitor" % self._serial)
+ self._changeState(self.STATE_CONNECTING)
+ self._timeout = get_new_timeout("communication")
+
+ while self._monitoring_active:
+ try:
+ line = self._readline()
+ if line is None:
+ break
+ if line.strip() is not "":
+ self._timeout = get_new_timeout("communication")
+ if line.startswith('<'): # status report
+ self._handle_status_report(line)
+ elif line.startswith('ok'): # ok message :)
+ self._handle_ok_message()
+ elif line.startswith('err'): # error message
+ self._handle_error_message(line)
+ elif line.startswith('ALA'): # ALARM message
+ self._handle_alarm_message(line)
+ elif line.startswith('['): # feedback message
+ self._handle_feedback_message(line)
+ elif line.startswith('Grb'): # Grbl startup message
+ self._handle_startup_message()
+ except:
+ self._logger.exception("Something crashed inside the monitoring loop, please report this to Mr. Beam")
+ errorMsg = "See octoprint.log for details"
+ self._log(errorMsg)
+ self._errorValue = errorMsg
+ self._changeState(self.STATE_ERROR)
+ eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
+ self._log("Connection closed, closing down monitor")
+
+ def _send_loop(self):
+ while self._sending_active:
+ try:
+ self._process_rt_commands()
+ if self.isPrinting() and self._commandQueue.empty():
+ cmd = self._getNext()
+ if cmd is not None:
+ self.sendCommand(cmd)
+ self._callback.on_comm_progress()
+ elif len(self._acc_line_buffer) == 0:
+ self._set_print_finished()
+
+ self._sendCommand()
+ self._send_event.wait(1)
+ self._send_event.clear()
+ except:
+ self._logger.exception("Something crashed inside the sending loop, please report this to Mr. Beam")
+ errorMsg = "See octoprint.log for details"
+ self._log(errorMsg)
+ self._errorValue = errorMsg
+ self._changeState(self.STATE_ERROR)
+ eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
+
+ def _metric_loop(self):
+ self._metricf = open('metric.tmp','w')
+ self._metricf.write("1 sec interval")
+ while self._metric_active:
+ time.sleep(1)
+ if self._metric_time is not None:
+ t = time.time()
+ #s = "Metric: %f [chars/sec]" % (self._metric_chars / (t - self._metric_time))
+ s = "%.2f" % (self._metric_chars / (t - self._metric_time))
+ self._metric_time = None
+ self._metric_chars = 0
+ self._metricf.write(s)
+ #self._log(s)
+ self._metricf.close()
+
+ def _sendCommand(self, cmd=None):
+ if cmd is None:
+ if self._cmd is None and self._commandQueue.empty():
+ return
+ elif self._cmd is None:
+ self._cmd = self._commandQueue.get()
+ if sum([len(x) for x in self._acc_line_buffer]) + len(self._cmd) +1 < self.RX_BUFFER_SIZE-5:
+ self._cmd, _, _ = self._process_command_phase("sending", self._cmd)
+ self._log("Send: %s" % self._cmd)
+ self._acc_line_buffer.append(self._cmd + '\n')
+ try:
+ self._serial.write(self._cmd + '\n')
+ self._process_command_phase("sent", self._cmd)
+ self._cmd = None
+ self._send_event.set()
+ except serial.SerialException:
+ self._log("Unexpected error while writing serial port: %s" % (get_exception_string()))
+ self._errorValue = get_exception_string()
+ self.close(True)
+ else:
+ cmd, _, _ = self._process_command_phase("sending", cmd)
+ self._log("Send: %s" % cmd)
+ try:
+
+ self._serial.write(cmd)
+ self._process_command_phase("sent", cmd)
+ except serial.SerialException:
+ self._log("Unexpected error while writing serial port: %s" % (get_exception_string()))
+ self._errorValue = get_exception_string()
+ self.close(True)
+
+ def _process_rt_commands(self):
+ if self._real_time_commands['poll_status']:
+ self._sendCommand('?')
+ self._real_time_commands['poll_status']=False
+ elif self._real_time_commands['feed_hold']:
+ self._sendCommand('!')
+ self._real_time_commands['feed_hold']=False
+ elif self._real_time_commands['cycle_start']:
+ self._sendCommand('~')
+ self._real_time_commands['cycle_start']=False
+ elif self._real_time_commands['soft_reset']:
+ self._sendCommand(b'\x18')
+ self._real_time_commands['soft_reset']=False
+
+ def _openSerial(self):
+ def default(_, port, baudrate, read_timeout):
+ if port is None or port == 'AUTO':
+ # no known port, try auto detection
+ self._changeState(self.STATE_DETECT_SERIAL)
+ ser = self._detectPort(True)
+ if ser is None:
+ self._errorValue = 'Failed to autodetect serial port, please set it manually.'
+ self._changeState(self.STATE_ERROR)
+ eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
+ self._log("Failed to autodetect serial port, please set it manually.")
+ return None
+ port = ser.port
+
+ # connect to regular serial port
+ self._log("Connecting to: %s" % port)
+ if baudrate == 0:
+ baudrates = baudrateList()
+ ser = serial.Serial(str(port), 115200 if 115200 in baudrates else baudrates[0], timeout=read_timeout, writeTimeout=10000, parity=serial.PARITY_ODD)
+ else:
+ ser = serial.Serial(str(port), baudrate, timeout=read_timeout, writeTimeout=10000, parity=serial.PARITY_ODD)
+ ser.close()
+ ser.parity = serial.PARITY_NONE
+ ser.open()
+ return ser
+
+ serial_factories = self._serial_factory_hooks.items() + [("default", default)]
+ for name, factory in serial_factories:
+ try:
+ serial_obj = factory(self, self._port, self._baudrate, settings().getFloat(["serial", "timeout", "connection"]))
+ except (OSError, serial.SerialException):
+ exception_string = get_exception_string()
+ self._errorValue = "Connection error, see Terminal tab"
+ self._changeState(self.STATE_ERROR)
+ eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
+ self._log("Unexpected error while connecting to serial port: %s %s (hook %s)" % (self._port, exception_string, name))
+ if "failed to set custom baud rate" in exception_string.lower():
+ self._log("Your installation does not support custom baudrates (e.g. 250000) for connecting to your printer. This is a problem of the pyserial library that OctoPrint depends on. Please update to a pyserial version that supports your baudrate or switch your printer's firmware to a standard baudrate (e.g. 115200). See https://github.com/foosel/OctoPrint/wiki/OctoPrint-support-for-250000-baud-rate-on-Raspbian")
+ return False
+ if serial_obj is not None:
+ # first hook to succeed wins, but any can pass on to the next
+ self._log(repr(self._serial))
+ self._changeState(self.STATE_OPEN_SERIAL)
+ self._serial = serial_obj
+ return True
+ return False
+
+ def _readline(self):
+ if self._serial is None:
+ return None
+ try:
+ ret = self._serial.readline()
+ self._send_event.set()
+ if('ok' in ret or 'error' in ret):
+ if(len(self._acc_line_buffer) > 0):
+ del self._acc_line_buffer[0] # Delete the commands character count corresponding to the last 'ok'
+ except serial.SerialException:
+ self._log("Unexpected error while reading serial port: %s" % (get_exception_string()))
+ self._errorValue = get_exception_string()
+ self.close(True)
+ return None
+ if ret == '': return ''
+ try:
+ self._log("Recv: %s" % sanitize_ascii(ret))
+ except ValueError as e:
+ self._log("WARN: While reading last line: %s" % e)
+ self._log("Recv: %r" % ret)
+ return ret
+
+ def _getNext(self):
+ if self._finished_currentFile is False:
+ line = self._currentFile.getNext()
+ if line is None:
+ self._finished_currentFile = True
+ return line
+ else:
+ return None
+
+ def _set_print_finished(self):
+ self._callback.on_comm_print_job_done()
+ self._changeState(self.STATE_OPERATIONAL)
+ payload = {
+ "file": self._currentFile.getFilename(),
+ "filename": os.path.basename(self._currentFile.getFilename()),
+ "origin": self._currentFile.getFileLocation(),
+ "time": self.getPrintTime()
+ }
+ eventManager().fire(Events.PRINT_DONE, payload)
+ self.sendCommand("M5")
+ self.sendCommand("G0X0Y0")
+ self.sendCommand("M9")
+
+ def _handle_status_report(self, line):
+ self._grbl_state = line[1:].split(',')[0]
+ if self._grbl_state == 'Queue':
+ if time.time() - self._pause_delay_time > 0.3:
+ if not self.isPaused():
+ self.setPause(True, False)
+ elif self._grbl_state == 'Run' or self._grbl_state == 'Idle':
+ if time.time() - self._pause_delay_time > 0.3:
+ if self.isPaused():
+ self.setPause(False, False)
+ self._update_grbl_pos(line)
+ #if self._metricf is not None:
+ # self._metricf.write(line)
+
+ def _handle_ok_message(self):
+ if self._state == self.STATE_HOMING:
+ self._changeState(self.STATE_OPERATIONAL)
+
+ def _handle_error_message(self, line):
+ self._errorValue = line
+ eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
+ self._changeState(self.STATE_LOCKED)
+
+ def _handle_alarm_message(self, line):
+ if "Hard/soft limit" in line:
+ errorMsg = "Machine Limit Hit. Please reset the machine and do a homing cycle"
+ self._log(errorMsg)
+ self._errorValue = errorMsg
+ eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
+ eventManager().fire(Events.LIMITS_HIT, {"error": self.getErrorString()})
+ elif "Abort during cycle" in line:
+ errorMsg = "Soft-reset detected. Please do a homing cycle"
+ self._log(errorMsg)
+ self._errorValue = errorMsg
+ eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
+ eventManager().fire(Events.SOFT_RESET, {"error": self.getErrorString()})
+ elif "Probe fail" in line:
+ errorMsg = "Probing has failed. Please reset the machine and do a homing cycle"
+ self._log(errorMsg)
+ self._errorValue = errorMsg
+ eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
+
+ with self._commandQueue.mutex:
+ self._commandQueue.queue.clear()
+ self._acc_line_buffer = []
+ self._send_event.clear(completely=True)
+ self._changeState(self.STATE_LOCKED)
+
+ # close and open serial port to reset arduino
+ self._serial.close()
+ self._openSerial()
+
+ def _handle_feedback_message(self, line):
+ if line[1:].startswith('Res'): # [Reset to continue]
+ pass
+ elif line[1:].startswith('\'$H'): # ['$H'|'$X' to unlock]
+ pass
+ elif line[1:].startswith('Cau'): # [Caution: Unlocked]
+ pass
+ elif line[1:].startswith('Ena'): # [Enabled]
+ pass
+ elif line[1:].startswith('Dis'): # [Disabled]
+ pass
+
+ def _handle_startup_message(self):
+ if self.isOperational():
+ errorMsg = "Machine reset."
+ self._cmd = None
+ self._acc_line_buffer = []
+ self._pauseWaitStartTime = None
+ self._pauseWaitTimeLost = 0.0
+ self._send_event.clear(completely=True)
+ with self._commandQueue.mutex:
+ self._commandQueue.queue.clear()
+ self._log(errorMsg)
+ self._errorValue = errorMsg
+ self._changeState(self.STATE_LOCKED)
+ eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
+ else:
+ self._onConnected(self.STATE_LOCKED)
+
+ def _update_grbl_pos(self, line):
+ # line example:
+ #
+ try:
+ idx_mx_begin = line.index('MPos:') + 5
+ idx_mx_end = line.index('.', idx_mx_begin) + 2
+ idx_my_begin = line.index(',', idx_mx_end) + 1
+ idx_my_end = line.index('.', idx_my_begin) + 2
+ #idx_mz_begin = line.index(',', idx_my_end) + 1
+ #idx_mz_end = line.index('.', idx_mz_begin) + 2
+
+ idx_wx_begin = line.index('WPos:') + 5
+ idx_wx_end = line.index('.', idx_wx_begin) + 2
+ idx_wy_begin = line.index(',', idx_wx_end) + 1
+ idx_wy_end = line.index('.', idx_wy_begin) + 2
+ #idx_wz_begin = line.index(',', idx_wy_end) + 1
+ #idx_wz_end = line.index('.', idx_wz_begin) + 2
+
+ #idx_intensity_begin = line.index('S:', idx_wz_end) + 2
+ #idx_intensity_end = line.index(',', idx_intensity_begin)
+
+ #idx_laserstate_begin = line.index('laser ', idx_intensity_end) + 6
+ #idx_laserstate_end = line.index(':', idx_laserstate_begin)
+
+ #payload = {
+ #"mx": line[idx_mx_begin:idx_mx_end],
+ #"my": line[idx_my_begin:idx_my_end],
+ #"mz": line[idx_mz_begin:idx_mz_end],
+ #"wx": line[idx_wx_begin:idx_wx_end],
+ #"wy": line[idx_wy_begin:idx_wy_end],
+ #"wz": line[idx_wz_begin:idx_wz_end],
+ #"laser": line[idx_laserstate_begin:idx_laserstate_end],
+ #"intensity": line[idx_intensity_begin:idx_intensity_end]
+ #}
+ mx = float(line[idx_mx_begin:idx_mx_end])
+ my = float(line[idx_my_begin:idx_my_end])
+ wx = float(line[idx_wx_begin:idx_wx_end])
+ wy = float(line[idx_wy_begin:idx_wy_end])
+ self._callback.on_comm_pos_update([mx, my, 0], [wx, wy, 0])
+ #eventManager().fire(Events.RT_STATE, payload)
+ except ValueError:
+ pass
+
+ def _process_command_phase(self, phase, command, command_type=None, gcode=None):
+ if phase not in ("queuing", "queued", "sending", "sent"):
+ return command, command_type, gcode
+
+ if gcode is None:
+ gcode = self._gcode_command_for_cmd(command)
+
+ # if it's a gcode command send it through the specific handler if it exists
+ if gcode is not None:
+ gcodeHandler = "_gcode_" + gcode + "_" + phase
+ if hasattr(self, gcodeHandler):
+ handler_result = getattr(self, gcodeHandler)(command, cmd_type=command_type)
+ command, command_type, gcode = self._handle_command_handler_result(command, command_type, gcode, handler_result)
+
+ # finally return whatever we resulted on
+ return command, command_type, gcode
+
+ def _gcode_command_for_cmd(self, cmd):
+ """
+ Tries to parse the provided ``cmd`` and extract the GCODE command identifier from it (e.g. "G0" for "G0 X10.0").
+
+ Arguments:
+ cmd (str): The command to try to parse.
+
+ Returns:
+ str or None: The GCODE command identifier if it could be parsed, or None if not.
+ """
+ if not cmd:
+ return None
+
+ if cmd == '!': return 'Hold'
+ if cmd == '~': return 'Resume'
+
+ gcode = self._regex_command.search(cmd)
+ if not gcode:
+ return None
+
+ return gcode.group(1)
+
+ # internal state management
+ def _changeState(self, newState):
+ if self._state == newState:
+ return
+
+ if newState == self.STATE_PRINTING:
+ if self._status_timer is not None:
+ self._status_timer.cancel()
+ self._status_timer = RepeatedTimer(1, self._poll_status)
+ self._status_timer.start()
+ elif newState == self.STATE_OPERATIONAL:
+ if self._status_timer is not None:
+ self._status_timer.cancel()
+ self._status_timer = RepeatedTimer(2, self._poll_status)
+ self._status_timer.start()
+ elif newState == self.STATE_PAUSED:
+ if self._status_timer is not None:
+ self._status_timer.cancel()
+ self._status_timer = RepeatedTimer(0.2, self._poll_status)
+ self._status_timer.start()
+
+ if newState == self.STATE_CLOSED or newState == self.STATE_CLOSED_WITH_ERROR:
+ if self._currentFile is not None:
+ self._currentFile.close()
+ self._log("entered state closed / closed with error. reseting character counter.")
+ self.acc_line_lengths = []
+
+ oldState = self.getStateString()
+ self._state = newState
+ self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
+ self._callback.on_comm_state_change(newState)
+
+ def _onConnected(self, nextState):
+ self._serial.timeout = settings().getFloat(["serial", "timeout", "communication"])
+
+ if(nextState is None):
+ self._changeState(self.STATE_LOCKED)
+ else:
+ self._changeState(nextState)
+
+ if not self.sending_thread.isAlive():
+ self.sending_thread.start()
+
+ payload = dict(port=self._port, baudrate=self._baudrate)
+ eventManager().fire(Events.CONNECTED, payload)
+
+ def _detectPort(self, close):
+ self._log("Serial port list: %s" % (str(serialList())))
+ for p in serialList():
+ try:
+ self._log("Connecting to: %s" % (p))
+ serial_obj = serial.Serial(p)
+ if close:
+ serial_obj.close()
+ return serial_obj
+ except (OSError, serial.SerialException) as e:
+ self._log("Error while connecting to %s: %s" % (p, str(e)))
+ return None
+
+ def _poll_status(self):
+ if self.isOperational():
+ self._real_time_commands['poll_status']=True
+ self._send_event.set()
+
+ def _soft_reset(self):
+ if self.isOperational():
+ self._real_time_commands['soft_reset']=True
+ self._send_event.set()
+
+ def _log(self, message):
+ self._callback.on_comm_log(message)
+ self._serialLogger.debug(message)
+
+ def _compareGrblVersion(self, versionDict):
+ cwd = os.path.dirname(__file__)
+ with open(cwd + "/../grbl/grblVersionRequirement.yml", 'r') as infile:
+ grblReqDict = yamlload(infile)
+ requiredGrblVer = str(grblReqDict['grbl']) + '_' + str(grblReqDict['git'])
+ if grblReqDict['dirty'] is True:
+ requiredGrblVer += '-dirty'
+ actualGrblVer = str(versionDict['grbl']) + '_' + str(versionDict['git'])
+ if versionDict['dirty'] is not(None):
+ actualGrblVer += '-dirty'
+ # compare actual and required grbl version
+ self._requiredGrblVer = requiredGrblVer
+ self._actualGrblVer = actualGrblVer
+ print repr(requiredGrblVer)
+ print repr(actualGrblVer)
+ if requiredGrblVer != actualGrblVer:
+ self._log("unsupported grbl version detected...")
+ self._log("required: " + requiredGrblVer)
+ self._log("detected: " + actualGrblVer)
+ return False
+ else:
+ return True
+
+ def _flashGrbl(self):
+ self._changeState(self.STATE_FLASHING)
+ self._serial.close()
+ cwd = os.path.dirname(__file__)
+ pathToGrblHex = cwd + "/../grbl/grbl.hex"
+
+ # TODO check if avrdude is installed.
+ # TODO log in logfile as well, not only to the serial monitor (use self._logger.info()... )
+ params = ["avrdude", "-patmega328p", "-carduino", "-b" + str(self._baudrate), "-P" + str(self._port), "-D", "-Uflash:w:" + pathToGrblHex]
+ rc = subprocesscall(params)
+
+ if rc is False:
+ self._log("successfully flashed new grbl version")
+ self._openSerial()
+ self._changeState(self.STATE_CONNECTING)
+ else:
+ self._log("error during flashing of new grbl version")
+ self._errorValue = "avrdude returncode: %s" % rc
+ self._changeState(self.STATE_CLOSED_WITH_ERROR)
+
+ @staticmethod
+ def _writeGrblVersionToFile(versionDict):
+ if versionDict['dirty'] == '-dirty':
+ versionDict['dirty'] = True
+ versionDict['lastConnect'] = time.time()
+ versionFile = os.path.join(settings().getBaseFolder("logs"), 'grbl_Version.yml')
+ with open(versionFile, 'w') as outfile:
+ outfile.write(yamldump(versionDict, default_flow_style=True))
+
+ def _handle_command_handler_result(self, command, command_type, gcode, handler_result):
+ original_tuple = (command, command_type, gcode)
+
+ if handler_result is None:
+ # handler didn't return anything, we'll just continue
+ return original_tuple
+
+ if isinstance(handler_result, basestring):
+ # handler did return just a string, we'll turn that into a 1-tuple now
+ handler_result = (handler_result,)
+ elif not isinstance(handler_result, (tuple, list)):
+ # handler didn't return an expected result format, we'll just ignore it and continue
+ return original_tuple
+
+ hook_result_length = len(handler_result)
+ if hook_result_length == 1:
+ # handler returned just the command
+ command, = handler_result
+ elif hook_result_length == 2:
+ # handler returned command and command_type
+ command, command_type = handler_result
+ else:
+ # handler returned a tuple of an unexpected length
+ return original_tuple
+
+ gcode = self._gcode_command_for_cmd(command)
+ return command, command_type, gcode
+
+ def _set_feedrate_override(self, value):
+ temp = value / 100.0
+ if temp > 0:
+ self._feedrate_factor = temp
+ self._feedrate_dict = {}
+ if self._actual_feedrate is not None:
+ temp = round(self._actual_feedrate * self._feedrate_factor)
+ # TODO replace with value from printer profile
+ if temp > 5000:
+ temp = 5000
+ self.sendCommand('F%d' % round(temp))
+
+ def _set_intensity_override(self, value):
+ temp = value / 100.0
+ if temp >= 0:
+ self._intensity_factor = temp
+ self._intensity_dict = {}
+ if self._actual_intensity is not None:
+ temp = round(self._actual_intensity * self._intensity_factor)
+ if temp > 1000:
+ temp = 1000
+ self.sendCommand('S%d' % round(temp))
+
+ def _replace_feedrate(self, cmd):
+ if self._feedrate_factor != 1:
+ obj = self._regex_feedrate.search(cmd)
+ if obj is not None:
+ feedrate_cmd = cmd[obj.start():obj.end()]
+ if feedrate_cmd in self._feedrate_dict:
+ new_feedrate = self._feedrate_dict[feedrate_cmd]
+ else:
+ self._actual_feedrate = int(feedrate_cmd[1:])
+ new_feedrate = round(self._actual_feedrate * self._feedrate_factor)
+ # TODO replace with value from printer profile
+ if new_feedrate > 5000:
+ new_feedrate = 5000
+ elif new_feedrate < 30:
+ new_feedrate = 30
+ self._feedrate_dict[feedrate_cmd] = new_feedrate
+ else:
+ return cmd
+ return cmd.replace(feedrate_cmd, 'F%d' % round(new_feedrate))
+ return cmd
+
+ def _replace_intensity(self, cmd):
+ if self._intensity_factor != 1:
+ obj = self._regex_intensity.search(cmd)
+ if obj is not None:
+ intensity_cmd = cmd[obj.start():obj.end()]
+ if intensity_cmd in self._intensity_dict:
+ new_intensity = self._intensity_dict[intensity_cmd]
+ else:
+ self._actual_intensity = int(intensity_cmd[1:])
+ new_intensity = round(self._actual_intensity * self._intensity_factor)
+ if new_intensity > 1000:
+ new_intensity = 1000
+ self._intensity_dict[intensity_cmd] = new_intensity
+ else:
+ return cmd
+ return cmd.replace(intensity_cmd, 'S%d' % round(new_intensity))
+ return cmd
+
+ ##~~ command handlers
+ def _gcode_G1_sending(self, cmd, cmd_type=None):
+ cmd = self._replace_feedrate(cmd)
+ cmd = self._replace_intensity(cmd)
+ return cmd
+
+ def _gcode_G2_sending(self, cmd, cmd_type=None):
+ cmd = self._replace_feedrate(cmd)
+ cmd = self._replace_intensity(cmd)
+ return cmd
+
+ def _gcode_G3_sending(self, cmd, cmd_type=None):
+ cmd = self._replace_feedrate(cmd)
+ cmd = self._replace_intensity(cmd)
+ return cmd
+
+ def _gcode_G01_sending(self, cmd, cmd_type=None):
+ return self._gcode_G1_sending(cmd, cmd_type)
+
+ def _gcode_G02_sending(self, cmd, cmd_type=None):
+ return self._gcode_G2_sending(cmd, cmd_type)
+
+ def _gcode_G03_sending(self, cmd, cmd_type=None):
+ return self._gcode_G3_sending(cmd, cmd_type)
+
+ def _gcode_H_sent(self, cmd, cmd_type=None):
+ self._changeState(self.STATE_HOMING)
+ return cmd
+
+ def _gcode_Hold_sent(self, cmd, cmd_type=None):
+ self._changeState(self.STATE_PAUSED)
+ return cmd
+
+ def _gcode_Resume_sent(self, cmd, cmd_type=None):
+ self._changeState(self.STATE_PRINTING)
+ return cmd
+
+ def sendCommand(self, cmd, cmd_type=None, processed=False):
+ cmd = cmd.encode('ascii', 'replace')
+ if not processed:
+ cmd = process_gcode_line(cmd)
+ if not cmd:
+ return
+
+ if cmd[0] == "/":
+ specialcmd = cmd[1:].lower()
+ if "togglestatusreport" in specialcmd:
+ if self._status_timer is None:
+ self._status_timer = RepeatedTimer(1, self._poll_status)
+ self._status_timer.start()
+ else:
+ self._status_timer.cancel()
+ self._status_timer = None
+ elif "setstatusfrequency" in specialcmd:
+ data = specialcmd[18:]
+ try:
+ frequency = float(data)
+ except ValueError:
+ self._log("No frequency setting found! Using 1 sec.")
+ frequency = 1
+ if self._status_timer is not None:
+ self._status_timer.cancel()
+ self._status_timer = RepeatedTimer(frequency, self._poll_status)
+ self._status_timer.start()
+ elif "disconnect" in specialcmd:
+ self.close()
+ elif "feedrate" in specialcmd:
+ data = specialcmd[8:]
+ self._set_feedrate_override(int(data))
+ elif "intensity" in specialcmd:
+ data = specialcmd[9:]
+ self._set_intensity_override(int(data))
+ else:
+ self._log("Command not Found! %s" % cmd)
+ self._log("available commands are:")
+ self._log(" /togglestatusreport")
+ self._log(" /setstatusfrequency ")
+ self._log(" /feedrate <%>")
+ self._log(" /intensity <%>")
+ self._log(" /disconnect")
+ return
+
+ eepromCmd = re.search("^\$[0-9]+=.+$", cmd)
+ if(eepromCmd and self.isPrinting()):
+ self._log("Warning: Configuration changes during print are not allowed!")
+
+ self._commandQueue.put(cmd)
+ self._send_event.set()
+
+ def selectFile(self, filename, sd):
+ if self.isBusy():
+ return
+
+ self._currentFile = PrintingGcodeFileInformation(filename)
+ eventManager().fire(Events.FILE_SELECTED, {
+ "file": self._currentFile.getFilename(),
+ "filename": os.path.basename(self._currentFile.getFilename()),
+ "origin": self._currentFile.getFileLocation()
+ })
+ self._callback.on_comm_file_selected(filename, self._currentFile.getFilesize(), False)
+
+ def unselectFile(self):
+ if self.isBusy():
+ return
+
+ self._currentFile = None
+ eventManager().fire(Events.FILE_DESELECTED)
+ self._callback.on_comm_file_selected(None, None, False)
+
+ def startPrint(self):
+ if not self.isOperational():
+ return
+
+ if self._currentFile is None:
+ raise ValueError("No file selected for printing")
+
+ try:
+ # ensure fan is on whatever gcode follows.
+ self.sendCommand("M08")
+
+ self._currentFile.start()
+ self._finished_currentFile = False
+
+ payload = {
+ "file": self._currentFile.getFilename(),
+ "filename": os.path.basename(self._currentFile.getFilename()),
+ "origin": self._currentFile.getFileLocation()
+ }
+ eventManager().fire(Events.PRINT_STARTED, payload)
+ #self.sendGcodeScript("beforePrintStarted", replacements=dict(event=payload))
+
+ self._changeState(self.STATE_PRINTING)
+ except:
+ self._logger.exception("Error while trying to start printing")
+ self._errorValue = get_exception_string()
+ self._changeState(self.STATE_ERROR)
+ eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
+
+ def cancelPrint(self):
+ if not self.isOperational():
+ return
+
+ with self._commandQueue.mutex:
+ self._commandQueue.queue.clear()
+ self._soft_reset()
+ self._acc_line_buffer = []
+ self._send_event.clear(completely=True)
+ self._changeState(self.STATE_LOCKED)
+
+ payload = {
+ "file": self._currentFile.getFilename(),
+ "filename": os.path.basename(self._currentFile.getFilename()),
+ "origin": self._currentFile.getFileLocation()
+ }
+
+ eventManager().fire(Events.PRINT_CANCELLED, payload)
+
+ def setPause(self, pause, send_cmd=True):
+ if not self._currentFile:
+ return
+
+ payload = {
+ "file": self._currentFile.getFilename(),
+ "filename": os.path.basename(self._currentFile.getFilename()),
+ "origin": self._currentFile.getFileLocation()
+ }
+
+ if not pause and self.isPaused():
+ if self._pauseWaitStartTime:
+ self._pauseWaitTimeLost = self._pauseWaitTimeLost + (time.time() - self._pauseWaitStartTime)
+ self._pauseWaitStartTime = None
+ self._pause_delay_time = time.time()
+ if send_cmd is True:
+ self._real_time_commands['cycle_start']=True
+ self._send_event.set()
+ eventManager().fire(Events.PRINT_RESUMED, payload)
+ elif pause and self.isPrinting():
+ if not self._pauseWaitStartTime:
+ self._pauseWaitStartTime = time.time()
+ self._pause_delay_time = time.time()
+ if send_cmd is True:
+ self._real_time_commands['feed_hold']=True
+ self._send_event.set()
+ eventManager().fire(Events.PRINT_PAUSED, payload)
+
+ def getStateString(self):
+ if self._state == self.STATE_NONE:
+ return "Offline"
+ if self._state == self.STATE_OPEN_SERIAL:
+ return "Opening serial port"
+ if self._state == self.STATE_DETECT_SERIAL:
+ return "Detecting serial port"
+ if self._state == self.STATE_CONNECTING:
+ return "Connecting"
+ if self._state == self.STATE_OPERATIONAL:
+ return "Operational"
+ if self._state == self.STATE_PRINTING:
+ return "Printing"
+ if self._state == self.STATE_PAUSED:
+ return "Paused"
+ if self._state == self.STATE_CLOSED:
+ return "Closed"
+ if self._state == self.STATE_ERROR:
+ return "Error: %s" % (self.getErrorString())
+ if self._state == self.STATE_CLOSED_WITH_ERROR:
+ return "Error: %s" % (self.getErrorString())
+ if self._state == self.STATE_LOCKED:
+ return "Locked"
+ if self._state == self.STATE_HOMING:
+ return "Homing"
+ if self._state == self.STATE_FLASHING:
+ return "Flashing"
+ return "?%d?" % (self._state)
+
+ def getPrintProgress(self):
+ if self._currentFile is None:
+ return None
+ return self._currentFile.getProgress()
+
+ def getPrintFilepos(self):
+ if self._currentFile is None:
+ return None
+ return self._currentFile.getFilepos()
+
+ def getCleanedPrintTime(self):
+ printTime = self.getPrintTime()
+ if printTime is None:
+ return None
+ return printTime
+
+ def getConnection(self):
+ return self._port, self._baudrate
+
+ def isOperational(self):
+ return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED
+
+ def isPrinting(self):
+ return self._state == self.STATE_PRINTING
+
+ def isPaused(self):
+ return self._state == self.STATE_PAUSED
+
+ def isLocked(self):
+ return self._state == self.STATE_LOCKED
+
+ def isHoming(self):
+ return self._state == self.STATE_HOMING
+
+ def isFlashing(self):
+ return self._state == self.STATE_FLASHING
+
+ def isBusy(self):
+ return self.isPrinting() or self.isPaused()
+
+ def isError(self):
+ return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR
+
+ def isClosedOrError(self):
+ return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED
+
+ def isSdReady(self):
+ return False
+
+ def isStreaming(self):
+ return False
+
+ def getErrorString(self):
+ return self._errorValue
+
+ def getPrintTime(self):
+ if self._currentFile is None or self._currentFile.getStartTime() is None:
+ return None
+ else:
+ return time.time() - self._currentFile.getStartTime() - self._pauseWaitTimeLost
+
+ def close(self, isError = False):
+ if self._status_timer is not None:
+ try:
+ self._status_timer.cancel()
+ self._status_timer = None
+ except AttributeError:
+ pass
+
+ self._monitoring_active = False
+ self._sending_active = False
+
+ printing = self.isPrinting() or self.isPaused()
+ if self._serial is not None:
+ if isError:
+ self._changeState(self.STATE_CLOSED_WITH_ERROR)
+ else:
+ self._changeState(self.STATE_CLOSED)
+ self._serial.close()
+ self._serial = None
+
+ if printing:
+ payload = None
+ if self._currentFile is not None:
+ payload = {
+ "file": self._currentFile.getFilename(),
+ "filename": os.path.basename(self._currentFile.getFilename()),
+ "origin": self._currentFile.getFileLocation()
+ }
+ eventManager().fire(Events.PRINT_FAILED, payload)
+ eventManager().fire(Events.DISCONNECTED)
+
+### MachineCom callback ################################################################################################
+class MachineComPrintCallback(object):
+ def on_comm_log(self, message):
+ pass
+
+ def on_comm_temperature_update(self, temp, bedTemp):
+ pass
+
+ def on_comm_state_change(self, state):
+ pass
+
+ def on_comm_message(self, message):
+ pass
+
+ def on_comm_progress(self):
+ pass
+
+ def on_comm_print_job_done(self):
+ pass
+
+ def on_comm_z_change(self, newZ):
+ pass
+
+ def on_comm_file_selected(self, filename, filesize, sd):
+ pass
+
+ def on_comm_sd_state_change(self, sdReady):
+ pass
+
+ def on_comm_sd_files(self, files):
+ pass
+
+ def on_comm_file_transfer_started(self, filename, filesize):
+ pass
+
+ def on_comm_file_transfer_done(self, filename):
+ pass
+
+ def on_comm_force_disconnect(self):
+ pass
+
+ def on_comm_pos_update(self, MPos, WPos):
+ pass
+
+class PrintingFileInformation(object):
+ """
+ Encapsulates information regarding the current file being printed: file name, current position, total size and
+ time the print started.
+ Allows to reset the current file position to 0 and to calculate the current progress as a floating point
+ value between 0 and 1.
+ """
+
+ def __init__(self, filename):
+ self._logger = logging.getLogger(__name__)
+ self._filename = filename
+ self._pos = 0
+ self._size = None
+ self._start_time = None
+
+ def getStartTime(self):
+ return self._start_time
+
+ def getFilename(self):
+ return self._filename
+
+ def getFilesize(self):
+ return self._size
+
+ def getFilepos(self):
+ return self._pos
+
+ def getFileLocation(self):
+ return FileDestinations.LOCAL
+
+ def getProgress(self):
+ """
+ The current progress of the file, calculated as relation between file position and absolute size. Returns -1
+ if file size is None or < 1.
+ """
+ if self._size is None or not self._size > 0:
+ return -1
+ return float(self._pos) / float(self._size)
+
+ def reset(self):
+ """
+ Resets the current file position to 0.
+ """
+ self._pos = 0
+
+ def start(self):
+ """
+ Marks the print job as started and remembers the start time.
+ """
+ self._start_time = time.time()
+
+ def close(self):
+ """
+ Closes the print job.
+ """
+ pass
+
+class PrintingGcodeFileInformation(PrintingFileInformation):
+ """
+ Encapsulates information regarding an ongoing direct print. Takes care of the needed file handle and ensures
+ that the file is closed in case of an error.
+ """
+
+ def __init__(self, filename, offsets_callback=None, current_tool_callback=None):
+ PrintingFileInformation.__init__(self, filename)
+
+ self._handle = None
+
+ self._first_line = None
+
+ self._offsets_callback = offsets_callback
+ self._current_tool_callback = current_tool_callback
+
+ if not os.path.exists(self._filename) or not os.path.isfile(self._filename):
+ raise IOError("File %s does not exist" % self._filename)
+ self._size = os.stat(self._filename).st_size
+ self._pos = 0
+
+ def start(self):
+ """
+ Opens the file for reading and determines the file size.
+ """
+ PrintingFileInformation.start(self)
+ self._handle = open(self._filename, "r")
+
+ def close(self):
+ """
+ Closes the file if it's still open.
+ """
+ PrintingFileInformation.close(self)
+ if self._handle is not None:
+ try:
+ self._handle.close()
+ except:
+ pass
+ self._handle = None
+
+ def getNext(self):
+ """
+ Retrieves the next line for printing.
+ """
+ if self._handle is None:
+ raise ValueError("File %s is not open for reading" % self._filename)
+
+ try:
+ processed = None
+ while processed is None:
+ if self._handle is None:
+ # file got closed just now
+ return None
+ line = self._handle.readline()
+ if not line:
+ self.close()
+ processed = process_gcode_line(line)
+ self._pos = self._handle.tell()
+
+ return processed
+ except Exception as e:
+ self.close()
+ self._logger.exception("Exception while processing line")
+ raise e
+
+def convert_pause_triggers(configured_triggers):
+ triggers = {
+ "enable": [],
+ "disable": [],
+ "toggle": []
+ }
+ for trigger in configured_triggers:
+ if not "regex" in trigger or not "type" in trigger:
+ continue
+
+ try:
+ regex = trigger["regex"]
+ t = trigger["type"]
+ if t in triggers:
+ # make sure regex is valid
+ re.compile(regex)
+ # add to type list
+ triggers[t].append(regex)
+ except re.error:
+ # invalid regex or something like this, we'll just skip this entry
+ pass
+
+ result = dict()
+ for t in triggers.keys():
+ if len(triggers[t]) > 0:
+ result[t] = re.compile("|".join(map(lambda pattern: "({pattern})".format(pattern=pattern), triggers[t])))
+ return result
+
+def process_gcode_line(line):
+ line = strip_comment(line).strip()
+ line = line.replace(" ", "")
+ if not len(line):
+ return None
+ return line
+
+def strip_comment(line):
+ if not ";" in line:
+ # shortcut
+ return line
+
+ escaped = False
+ result = []
+ for c in line:
+ if c == ";" and not escaped:
+ break
+ result += c
+ escaped = (c == "\\") and not escaped
+ return "".join(result)
+
+def get_new_timeout(t):
+ now = time.time()
+ return now + get_interval(t)
+
+def get_interval(t):
+ if t not in default_settings["serial"]["timeout"]:
+ return 0
+ else:
+ return settings().getFloat(["serial", "timeout", t])
+
+def serialList():
+ baselist = []
+ baselist = baselist \
+ + glob.glob("/dev/ttyUSB*") \
+ + glob.glob("/dev/ttyACM*") \
+ + glob.glob("/dev/tty.usb*") \
+ + glob.glob("/dev/cu.*") \
+ + glob.glob("/dev/cuaU*") \
+ + glob.glob("/dev/rfcomm*")
+
+ additionalPorts = settings().get(["serial", "additionalPorts"])
+ for additional in additionalPorts:
+ baselist += glob.glob(additional)
+
+ prev = settings().get(["serial", "port"])
+ if prev in baselist:
+ baselist.remove(prev)
+ baselist.insert(0, prev)
+ if settings().getBoolean(["devel", "virtualPrinter", "enabled"]):
+ baselist.append("VIRTUAL")
+ return filter(None, baselist)
+
+def baudrateList():
+ ret = [250000, 230400, 115200, 57600, 38400, 19200, 9600]
+ prev = settings().getInt(["serial", "baudrate"])
+ if prev in ret:
+ ret.remove(prev)
+ ret.insert(0, prev)
+ return ret