当前位置:首页 > 嵌入式 > 嵌入式软件
[导读]基于Android嵌入式平台传感器应用开发水平仪

摘要:详细介绍了如何利用Android系统的传感器开发水平仪应用的全过程。通过对该案例开发的讲解,介绍了传感器应用的开发方法以及通过Eclipse开发Android应用程序的过程。

1Android平台简介

互联网巨头Google公司于2007年11月5日推出了全新的嵌入式软件平台---Android,该平台由操作系统、中间件、用户界面以及应用软件组成,是一个真正开放的移动应用开发平台。

2007年11月初,Google与其他33家手机厂商、软硬件供应商、手机芯片供应商、移动运营商联合组成了开放手机联盟(OpenHandsetAlliance),发布了名为Android的手机软件平台,并宣布该平台完全开放。同时Google希望Android平台成为一套标准化、开放式的移动嵌入式软件平台。

由于Android系统具有开发性、平等性、无界性以及方便性等优点,所以很快被业界所接受。从2008年初开始,越来越多的开发人员投身到Android应用的开发当中。

而Android系统的一大亮点之一就是传感器的使用,利用传感器可以开发出很多新奇有趣的应用程序。例如计步器、水平仪,甚至在很多游戏中都可以使用传感器来操作游戏。传感器的种类有很多种,其中包括加速度传感器、姿态传感器、磁场传感器、温度传感器以及光传感器等,介绍的水平仪应用就是使用姿态传感器的。

2案例功能

将结合水平仪案例的开发详细介绍如何在Android平台下开发传感器应用,下面首先对水平仪的功能及界面进行简单的介绍。

2。1程序界面

程序运行后的效果如图1所示,用户可以通过调整手机的姿态来控制界面中各个气泡的位置。与真正的水平仪一样,在使用手机水平仪时,需要将手机平放到某个平面上才可以。

 

图1 水平仪应用程序界面

2。2软件功能

运行该程序,当改变手机的姿态时,界面中的气泡便会根据手机的姿态向高处进行相应的移动。

当手机所处的平面水平时,各个气泡都应该位于中间的指定区域。

3开发环境搭建

正式进入代码开发之前,首先需要对开发环境进行搭建,其搭建步骤如下所列。

(1)安装Java开发环境JDK。

(2)从网上下载Android开发环境SDK的压缩包,并将其解压到磁盘上的某个位置。

(3)将SDK解压目录中的tools目录添加到系统的PATH环境变量中。

(4)下载并安装Eclipse集成开发环境。

(5)为Eclipse安装Android开发插件ADT,并在Eclipse的Preferences中配置Android插件的SDKLocation。

(6)在Eclipse的AVDManager中创建Android虚拟设备(AVD),并启动模拟器。

(7)下载并安装用来调试Android传感器应用的Sensorsimulator传感器模拟器软件。

(8)在模拟器中安装Sensorsimulator所对应的apk文件并对其进行调试使Sensorsimulator应用程序能够与Android模拟器进行通信。

4开发前的准备

前面完成了开发环境的搭建,但在正式进行代码开发之前,还需要再做一些开发前的准备工作,其步骤如下:

(1)首先启动之前安装好Eclipse。

(2)然后依次点击File|New|Other|Android|AndroidProject进入项目的创建界面。

(3)在项目创建界面中,输入项目的名称、所使用的目标平台、所在的包名等信息,如图2所示。

图2 在Eclipse 中创建Android 项目

(4)点击"Finish"完成项目的创建。

(5)在程序中将会用到的图片资源存放到项目文件夹的res/drawable-mdpi目录下,如图3所示。

图3 图片资源

(6)为应用程序引入调试时使用的Sensorsimulator支持jar包,该jar包位于Sensorsimulator安装目录中的bin目录下:

5自定义View的开发

本案例需要自定义一个View来绘制水平仪的用户界面,首先需要在项目文件夹的src/wyf/ytl目录下创建一个名为Main-View的java类,并使其继承自View类,其代码框架如下:

packagewyf。ytl;//声明所在包

importandroid。content。Context;//引入Context类

importandroid。graphics。Bitmap;//引入Bitmap类

importandroid。graphics。BitmapFactory;//引入相关类

importandroid。graphics。Canvas;//引入Canvas类

importandroid。graphics。Color;//引入Color类

importandroid。graphics。Paint;//引入Paint类

importandroid。graphics。RectF;//引入RectF类

importandroid。graphics。Paint。Style;//引入Style类

importandroid。util。AttributeSet;//引入AttributeSet类

importandroid。view。View;//引入View类

publicclassMainViewextendsView{

Paintpaint=newPaint();//画笔

//图片资源的声明

BitmapshangBitmap1;//上面的大矩形图

BitmapshangBitmap2;//上面的气泡

BitmapzuoBitmap1;//左面的大矩形图

BitmapzuoBitmap2;//左面图的气泡

BitmapzhongBitmap1;//中间的大圆图

BitmapzhongBitmap2;//中间的小气泡

BitmapxiaBitmap1;//右下的矩形图[!--empirenews.page--]

BitmapxiaBitmap2;//右下的气泡

//背景矩形的位置声明

intshang1_X=60;//上面的大矩形图

intshang1_Y=12;

intzuo1_X=12;//左面的大矩形图

intzuo1_Y=60;

intzhong1_X=65;//中间的大圆图

intzhong1_Y=65;

intxia1_X=145;//右下的矩形图

intxia1_Y=145;//水泡的位置声明

intshang2_X;//上面的气泡XY坐标

intshang2_Y;

intzuo2_X;//左面图的气泡XY坐标

intzuo2_Y;

intzhong2_X;//中间的小气泡XY坐标

intzhong2_Y;

intxia2_X;//右下的气泡XY坐标

intxia2_Y;

publicMainView(Contextcontext,AttributeSetattrs){

super(context,attrs);

initBitmap();//初始化图片资源

initLocation();//初始化气泡的位置

}

privatevoidinitBitmap(){//初始化图片的方法

…//该处省略了部分代码,将在后面进行介绍

}

privatevoidinitLocation(){//初始化气泡位置的方法

…//该处省略了部分代码,将在后面进行介绍

}

@Override

protectedvoidonDraw(Canvascanvas){//重写的绘制方法

super。onDraw(canvas);

…//该处省略了部分代码,将在后面进行介绍

}

}

上述代码中的initBitmap以及initLocation方法是对界面进行初始化方法,而onDraw方法会根据需要绘制整个界面。

MainView类构造器中调用了两个单独的方法对整个界面进行了初始化,这是一种非常好的编程习惯。因为把不同功能的代码各自编写成独立的方法可以使主逻辑清晰,且各个方法中的代码都不是很长,会大大提高代码的可读性以及可维护性。

完成了代码框架的开发后就可以对其中各个方法进行开发了,首先开发的是图片资源的初始化方法,其代码如下:

privatevoidinitBitmap(){//初始化图片资源的方法

shangBitmap1=BitmapFactory。decodeResource(getResources(),R。drawable。shang1);

shangBitmap2=BitmapFactory。decodeResource(getResources(),R。drawable。shang2);

zuoBitmap1=BitmapFactory。decodeResource(getResources(),R。drawable。zuo1);

zuoBitmap2=BitmapFactory。decodeResource(getResources(),R。drawable。zuo2);

zhongBitmap1=BitmapFactory。decodeResource(getResources(),R。drawable。zhong1);

zhongBitmap2=BitmapFactory。decodeResource(getResources(),R。drawable。zhong2);

xiaBitmap1=BitmapFactory。decodeResource(getResources(),R。drawable。xia1);

xiaBitmap2=BitmapFactory。decodeResource(getResources(),R。drawable。xia2);

}

上述代码为initBitmap方法的全部代码,其作用是对程序中所有的图片资源进行初始化,在开发该方法之前,应该确保所有的图片资源已经存放到了指定的目录下。

完成了图片资源初始化方法的开发后,便可对气泡位置初始化方法initLocation进行开发了,其代码如下:

privatevoidinitLocation(){//初始化气泡位置的方法

shang2_X=shang1_X+shangBitmap1。getWidth()/2-shangBitmap2。getWidth()/2;

shang2_Y=shang1_Y+shangBitmap1。getHeight()/2-shangBitmap2。getHeight()/2;

zuo2_X=zuo1_X+zuoBitmap1。getWidth()/2-zuoBitmap2。getWidth()/2;

zuo2_Y=zuo1_Y+zuoBitmap1。getHeight()/2-zuoBitmap2。getHeight()/2;

zhong2_X=zhong1_X+zhongBitmap1。getWidth()/2-zhongBitmap2。getWidth()/2;

zhong2_Y=zhong1_Y+zhongBitmap1。getHeight()/2-zhongBitmap2。getHeight()/2;

xia2_X=xia1_X+xiaBitmap1。getWidth()/2-xiaBitmap2。getWidth()/2;

xia2_Y=xia1_Y+xiaBitmap1。getHeight()/2-xiaBitmap2。getHeight()/2;

}

在该方法中通过相应图片的宽度和高度动态计算气泡的初始坐标,采用此方法动态计算气泡坐标的好处是当日后更改图片资源后,不需要重写修改源代码即可正常运行,大大提高了程序的可维护性。

在完成了各个初始化方法的开发后就可以对绘制方法onDraw进行开发,该方法主要负责界面的绘制工作,其代码如下:

@Override

protectedvoidonDraw(Canvascanvas){//界面绘制方法super。onDraw(canvas);

canvas。drawColor(Color。WHITE);//设置背景色为白色

paint。setColor(Color。BLUE);//设置画笔颜色

paint。setStyle(Style。STROKE);//设置画笔为不填充

canvas。drawRect(5,5,315,315,paint);//绘制外边框矩形

//画背景矩形

canvas。drawBitmap(shangBitmap1,shang1_X,shang1_Y,paint);//上

canvas。drawBitmap(zuoBitmap1,zuo1_X,zuo1_Y,paint);//左

canvas。drawBitmap(zhongBitmap1,zhong1_X,zhong1_Y,paint);//中

canvas。drawBitmap(xiaBitmap1,xia1_X,xia1_Y,paint);//下

//开始绘制气泡

canvas。drawBitmap(shangBitmap2,shang2_X,shang2_Y,paint);//上

canvas。drawBitmap(zuoBitmap2,zuo2_X,zuo2_Y,paint);//左

canvas。drawBitmap(zhongBitmap2,zhong2_X,zhong2_Y,paint);//中

canvas。drawBitmap(xiaBitmap2,xia2_X,xia2_Y,paint);//下

paint。setColor(Color。GRAY);//设置画笔颜色用来绘制刻度

//绘制上面方框中的刻度

canvas。drawLine(shang1_X+shangBitmap1。getWidth()/2-7,shang1_Y,shang1_X+shangBitmap1。getWidth()/2-7,shang1_Y+shangBitmap1。getHeight()-2,paint);

canvas。drawLine(shang1_X+shangBitmap1。getWidth()/2+7,shang1_Y,shang1_X+shangBitmap1。getWidth()/2+7,shang1_Y+shangBitmap1。getHeight()-2,paint);[!--empirenews.page--]

//绘制左面方框中的刻度

canvas。drawLine(zuo1_X,zuo1_Y+zuoBitmap1。getHeight()/2-7,zuo1_X+zuoBitmap1。getWidth()-2,zuo1_Y+zuoBitmap1。getHeight()/2-7,paint);canvas。drawLine(zuo1_X,zuo1_Y+zuoBitmap1。getHeight()/2+7,zuo1_X+zuoBitmap1。getWidth()-2,zuo1_Y+zuoBitmap1。getHeight()/2+7,paint);

//绘制下面方框中的刻度

canvas。drawLine(xia1_X+xiaBitmap1。getWidth()/2-10,xia1_Y+xiaBitmap1。getHeight()/2-20,xia1_X+xiaBitmap1。getWidth()/2+20,xia1_Y+xiaBitmap1。getHeight()/2+10,paint);

canvas。drawLine(xia1_X+xiaBitmap1。getWidth()/2-20,xia1_Y+xiaBitmap1。getHeight()/2-10,xia1_X+xiaBitmap1。getWidth()/2+10,xia1_Y+xiaBitmap1。getHeight()/2+20,paint);

//中间圆圈中的刻度(小圆)

RectFoval=newRectF(zhong1_X+zhongBitmap1。getWidth()/2-10,zhong1_Y+zhongBitmap1。getHeight()/2-10,zhong1_X+zhongBitmap1。getWidth()/2+10,zhong1_Y+zhongBitmap1。getHeight()/2+10);

canvas。drawOval(oval,paint);//绘制基准线(圆)

}

在该方法中,根据相应图片的X、Y坐标将图片绘制到屏幕中,在图片的绘制过程中,同样动态根据相应图片的宽和高计算需要绘制到的位置坐标,以提高程序的可维护性与灵活性。

6相关XML文件的编写

完成了用于显示水平仪界面的自定义View的Java代码开发之后,就应该对布局XML资源文件进行编写,以将之前开发的自定义View添加到用户界面中。打开项目中res/layout目录下的main。xml,在其中编写如下的xml代码:

<?xmlversion="1。0"encoding="utf-8"?><!--编码格式-->

 

android:orientation="vertical"

android:layout_width="fill_parent"

android:layout_height="fill_parent"><!--线性布局-->

 

android:id="@+id/mainView"

android:layout_width="fill_parent"

android:layout_height="fill_parent"/><!--自定义

View-->

</LinearLayout>

编写完布局文件main。xml后,还需要开发字符串资源文件strings。xml。打开res/values下的strings。xml文件,编写如下的代码:

<?xmlversion="1。0"encoding="utf-8"?><!--编码方式-->

水平仪

在该文件中只是对字符串app_name进行了定义,在开发Android应用程序时,将字符串资源统一定义到一个xml文件中是一个很好的编程习惯。

编写完上述的xml资源文件后,为了调试还需要为此应用程序添加网络权限,打开项目根目录下的AndroidManifest。xml文件,在""标签之前加入下列代码:

 

上述代码的功能为此应用程序添加了访问网络的权限。

7Activity类的开发

完成了自定义View以及XML文件的开发后,就可以对用户界面对应的Activity类进行开发,首先开发该类的代码框架,其代码如下:

packagewyf。ytl;//声明所在包

importandroid。app。Activity;//引入相关类

importandroid。hardware。SensorListener;

importandroid。hardware。SensorManager;

importandroid。os。Bundle;

publicclassSPYActivityextendsActivity{//继承ActivityMainViewmv;//主View

intk=45;//灵敏度

//SensorManagermySensorManager;

//真机

SensorManagerSimulatormySensorManager;//测试时@Override

publicvoidonCreate(BundlesavedInstanceState){super。onCreate(savedInstanceState);

setContentView(R。layout。main);//设置当前用户界面

mv=(MainView)findViewById(R。id。mainView);

mySensorManager=SensorManagerSimulator。getSystemService(this,SENSOR_SERVICE);//测试时

mySensorManager。connectSimulator();//测试时

//mySensorManager=(SensorManager)

//getSystemService(SENSOR_SERVICE);//真机

}

privatefinalSensorListenermSensorLisener=newSensorListener(){//传感器监听

//器类

…//该处省略了部分代码,将在后面进行介绍

};

@Override

protectedvoidonResume(){//添加监听

mySensorManager。registerListener(mSensorLisener,SensorManager。SENSOR_ORIENTATION);

super。onResume();

}

@Override

protectedvoidonPause(){//取消监听

mySensorManager。unregisterListener(mSensorLisener);

super。onPause();

}

}

上述代码中除了重写了onCreate方法外,还重写了onRe-sume以及onPause方法为mySensorManager添加或删除监听,并且定义了传感器监听器类mSensorLisener。

在完成了Activity类代码框架的开发后就可以对其中传感器的监听类进行开发,首先给出监听器类的代码框架:

privatefinalSensorListenermSensorLisener=

newSensorListener(){//传感器监听器类

publicvoidonSensorChanged(intsensor,float[]values){…//该处省略了部分代码,将在后面进行介绍

}

@Override

publicvoidonAccuracyChanged(intsensor,intaccuracy){}

publicbooleanisContain(intx,inty){//判断点是否在圆内

inttempx=(int)(x+mv。zhongBitmap2。getWidth()/2。0);

[!--empirenews.page--]

inttempy=(int)(y+mv。zhongBitmap2。getWidth()/2。0);

intox=(int)(mv。zhong1_X+mv。zhongBitmap1。getWidth()/2。0);

intoy=(int)(mv。zhong1_X+mv。zhongBitmap1。getWidth()/2。0);

if(Math。sqrt((tempx-ox)*(tempx-ox)+(tempy-oy)*(tempy-oy))>(mv。zhongBitmap1。getWidth()/2。0-mv。zhongBitmap2。getWidth()/2。0)){//不在圆内returnfalse;

}else{//在圆内时

returntrue;

}

}

};

在传感器监听类中,onSensorChanged方法用于监听传感器采样值的变化,例如手机姿态的改变等。上述代码中的is-Contain方法用于判断界面中间的气泡是否出界,若出界则返回false。

完成了代码框架的开发后,便可以对传感器的监听方法onSensorChanged进行开发了,其详细代码如下:

publicvoidonSensorChanged(intsensor,float[]values){

if(sensor==SensorManager。SENSOR_ORIENTATION){

doublepitch=values[SensorManager。DATA_Y];

doubleroll=values[SensorManager。DATA_Z];

intx=0;inty=0;//临时变量,算中间水泡坐标时用

inttempX=0;inttempY=0;//下面气泡的临时变量

//开始调整x的值

if(Math。abs(roll)<=k){

mv。shang2_X=mv。shang1_X//上面的

+(int)(((mv。shangBitmap1。getWidth()

-mv。shangBitmap2。getWidth())/2。0)

-(((mv。shangBitmap1。getWidth()

-mv。shangBitmap2。getWidth())/2。0)*roll)/k);

x=mv。zhong1_X//中间的

+(int)(((mv。zhongBitmap1。getWidth()

-mv。zhongBitmap2。getWidth())/2。0)

-(((mv。zhongBitmap1。getWidth()

-mv。zhongBitmap2。getWidth())/2。0)*roll)/k);

}elseif(roll>k){

mv。shang2_X=mv。shang1_X;x=mv。zhong1_X;

}else{

mv。shang2_X=mv。shang1_X+

mv。shangBitmap1。getWidth()

-mv。shangBitmap2。getWidth();

x=mv。zhong1_X+mv。zhongBitmap1。getWidth()

-mv。zhongBitmap2。getWidth();

}

//开始调整y的值

if(Math。abs(pitch)<=k){

mv。zuo2_Y=mv。zuo1_Y//左面的

+(int)(((mv。zuoBitmap1。getHeight()

-mv。zuoBitmap2。getHeight())/2。0)

+(((mv。zuoBitmap1。getHeight()

-mv。zuoBitmap2。getHeight())/2。0)*pitch)/k);

y=mv。zhong1_Y+//中间的

(int)(((mv。zhongBitmap1。getHeight()

-mv。zhongBitmap2。getHeight())/2。0)

+(((mv。zhongBitmap1。getHeight()

-mv。zhongBitmap2。getHeight())/2。0)*pitch)/k);

}elseif(pitch>k){

mv。zuo2_Y=mv。zuo1_Y

+mv。zuoBitmap1。getHeight()

-mv。zuoBitmap2。getHeight();

y=mv。zhong1_Y+mv。zhongBitmap1。getHeight()

-mv。zhongBitmap2。getHeight();

}else{

mv。zuo2_Y=mv。zuo1_Y;y=mv。zhong1_Y;

}

//下面的

tempX=-(int)(((mv。xiaBitmap1。getWidth()/2-28)*roll

+(mv。xiaBitmap1。getWidth()/2-28)*pitch)/k);

tempY=-(int)((-(mv。xiaBitmap1。getWidth()/2-28)*roll

-(mv。xiaBitmap1。getWidth()/2-28)*pitch)/k);

//限制下面的气泡范围

if(tempY>mv。xiaBitmap1。getHeight()/2-28){

tempY=mv。xiaBitmap1。getHeight()/2-28;

}

if(tempY<-mv。xiaBitmap1。getHeight()/2+28){

tempY=-mv。xiaBitmap1。getHeight()/2+28;

}

if(tempX>mv。xiaBitmap1。getWidth()/2-28){

tempX=mv。xiaBitmap1。getWidth()/2-28;

}

if(tempX<-mv。xiaBitmap1。getWidth()/2+28){

tempX=-mv。xiaBitmap1。getWidth()/2+28;

}

mv。xia2_X=tempX+mv。xia1_X

+mv。xiaBitmap1。getWidth()/2

-mv。xiaBitmap2。getWidth()/2;

mv。xia2_Y=tempY+mv。xia1_Y

+mv。xiaBitmap1。getHeight()/2

-mv。xiaBitmap2。getWidth()/2;

if(isContain(x,y)){//中间的水泡在圆内才改变坐标

mv。zhong2_X=x;mv。zhong2_Y=y;

}

mv。postInvalidate();//重绘MainView

}

}

在onSensorChanged方法中首先得到pitch轴以及roll轴的数值,然后根据该数值的大小调整水泡在屏幕中的位置,同时需要对水泡的坐标进行判断,使其保持在自身所在外框的范围内。

此时运行该程序,并保证测试工具Sensorsimulator与Android模拟器的连通,便会观察到如图1所示的效果,通过Sensorsimulator工具模拟手机的姿态的改变,屏幕中的水泡便随之向高处运动。

8程序发布

完成了所有代码的开发后,就可以将应用程序打包发布了。本案例中只需将Eclipse工具自动生成的apk文件拷出即可,按如下步骤操作。

(1)进行正式发布之前首先需要将代码中注释为"测试时使用"的两处代码删掉,并将注释为"真机使用"代码的注释去掉。

(2)完成代码的修改后重新构建项目。

(3)打开项目文件夹下的bin目录,其中名为SPY的apk文件便为本应用程序的安装包。

(4)将SPY。apk文件拷贝到支持传感器的Android手机中运行即可完成本应用程序的安装。

9结语

通过开发基于Android平台的传感器应用---水平仪程序,读者应该对Android程序的开发有了一定的了解,同时读者也应该了解到在Android平台下使用传感器来丰富自己软件的功能是十分方便的。[!--empirenews.page--]

另外,本案例虽然只对姿态传感器进行了应用,但相信通过对本案例的学习,读者已经有能力对其他传感器进行应用,开发出更具新意的吸引人的其他应用程序。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

9月2日消息,不造车的华为或将催生出更大的独角兽公司,随着阿维塔和赛力斯的入局,华为引望愈发显得引人瞩目。

关键字: 阿维塔 塞力斯 华为

加利福尼亚州圣克拉拉县2024年8月30日 /美通社/ -- 数字化转型技术解决方案公司Trianz今天宣布,该公司与Amazon Web Services (AWS)签订了...

关键字: AWS AN BSP 数字化

伦敦2024年8月29日 /美通社/ -- 英国汽车技术公司SODA.Auto推出其旗舰产品SODA V,这是全球首款涵盖汽车工程师从创意到认证的所有需求的工具,可用于创建软件定义汽车。 SODA V工具的开发耗时1.5...

关键字: 汽车 人工智能 智能驱动 BSP

北京2024年8月28日 /美通社/ -- 越来越多用户希望企业业务能7×24不间断运行,同时企业却面临越来越多业务中断的风险,如企业系统复杂性的增加,频繁的功能更新和发布等。如何确保业务连续性,提升韧性,成...

关键字: 亚马逊 解密 控制平面 BSP

8月30日消息,据媒体报道,腾讯和网易近期正在缩减他们对日本游戏市场的投资。

关键字: 腾讯 编码器 CPU

8月28日消息,今天上午,2024中国国际大数据产业博览会开幕式在贵阳举行,华为董事、质量流程IT总裁陶景文发表了演讲。

关键字: 华为 12nm EDA 半导体

8月28日消息,在2024中国国际大数据产业博览会上,华为常务董事、华为云CEO张平安发表演讲称,数字世界的话语权最终是由生态的繁荣决定的。

关键字: 华为 12nm 手机 卫星通信

要点: 有效应对环境变化,经营业绩稳中有升 落实提质增效举措,毛利润率延续升势 战略布局成效显著,战新业务引领增长 以科技创新为引领,提升企业核心竞争力 坚持高质量发展策略,塑强核心竞争优势...

关键字: 通信 BSP 电信运营商 数字经济

北京2024年8月27日 /美通社/ -- 8月21日,由中央广播电视总台与中国电影电视技术学会联合牵头组建的NVI技术创新联盟在BIRTV2024超高清全产业链发展研讨会上宣布正式成立。 活动现场 NVI技术创新联...

关键字: VI 传输协议 音频 BSP

北京2024年8月27日 /美通社/ -- 在8月23日举办的2024年长三角生态绿色一体化发展示范区联合招商会上,软通动力信息技术(集团)股份有限公司(以下简称"软通动力")与长三角投资(上海)有限...

关键字: BSP 信息技术
关闭
关闭