Coder Social home page Coder Social logo

bleps's Introduction

bleps - A toy-level BLE peripheral stack

This is a BLE peripheral stack in Rust. (no-std / no-alloc)

To use it you need an implementation of embedded-io offering communication with HCI.

The goal is just to have something that works for testing,demos and personal projects - no intentions to make this pass certification tests.

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or

bleps's People

Contributors

bjoernq avatar brandonros avatar bugadani avatar dzamlo avatar dzhu avatar jessebraham avatar jneem avatar katyo avatar konkers avatar mabezdev avatar matmaul avatar one-d-wide avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

bleps's Issues

How to change initial GATT Service?

Hello I have an ESP-32H2 which I successfully tested basically with the example Attribute Server with the GATT Profile (including some embassy), but I seem to be unable to define a new Service after initial flash. I use espflash but i explicitly use it with --no-skip so it should erase and reflash every time.

#![no_std]
#![no_main]
#![feature(type_alias_impl_trait)]
#![feature(async_closure)]

// BLE Example
use core::cell::RefCell;

use bleps::{
    ad_structure::{
        create_advertising_data, AdStructure, BR_EDR_NOT_SUPPORTED, LE_GENERAL_DISCOVERABLE,
    },
    async_attribute_server::AttributeServer,
    asynch::Ble,
    attribute_server::NotificationData,
    gatt,
};
use embassy_executor::Spawner;
use esp_backtrace as _;
use esp_hal::{
    clock::ClockControl, embassy, gpio::IO, peripherals::*, prelude::*, rng::Rng, timer::TimerGroup,
};
use esp_println::println;
use esp_wifi::{ble::controller::asynch::BleConnector, initialize, EspWifiInitFor};

pub type BootButton = esp_hal::gpio::Gpio9<esp_hal::gpio::Input<esp_hal::gpio::PullDown>>;
pub const SOC_NAME: &str = "ESP32-H2";

#[main]
async fn main(_spawner: Spawner) -> ! {
    #[cfg(feature = "log")]
    esp_println::logger::init_logger(log::LevelFilter::Info);

    let peripherals = Peripherals::take();

    let system = peripherals.SYSTEM.split();
    let clocks = ClockControl::max(system.clock_control).freeze();

    #[cfg(target_arch = "xtensa")]
    let timer = hal::timer::TimerGroup::new(peripherals.TIMG1, &clocks, None).timer0;
    #[cfg(target_arch = "riscv32")]
    let timer = esp_hal::systimer::SystemTimer::new(peripherals.SYSTIMER).alarm0;
    let init = initialize(
        EspWifiInitFor::Ble,
        timer,
        Rng::new(peripherals.RNG),
        system.radio_clock_control,
        &clocks,
    )
    .unwrap();

    let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
    let button = io.pins.gpio9.into_pull_down_input();

    // Async requires the GPIO interrupt to wake futures
    esp_hal::interrupt::enable(
        esp_hal::peripherals::Interrupt::GPIO,
        esp_hal::interrupt::Priority::Priority1,
    )
    .unwrap();

    let timer_group0 = TimerGroup::new_async(peripherals.TIMG0, &clocks);
    embassy::init(&clocks, timer_group0);

    let mut bluetooth = peripherals.BT;

    let connector = BleConnector::new(&init, &mut bluetooth);
    let mut ble = Ble::new(connector, esp_wifi::current_millis);
    println!("Connector created");

    let pin_ref = RefCell::new(button);
    let pin_ref = &pin_ref;

    loop {
        println!("{:?}", ble.init().await);
        println!("{:?}", ble.cmd_set_le_advertising_parameters().await);
        println!(
            "{:?}",
            ble.cmd_set_le_advertising_data(
                create_advertising_data(&[
                    AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED),
                    AdStructure::ServiceUuids16(&[Uuid::Uuid16(0x1809)]),
                    AdStructure::CompleteLocalName(SOC_NAME),
                ])
                .unwrap()
            )
            .await
        );
        println!("{:?}", ble.cmd_set_le_advertise_enable(true).await);

        println!("started advertising");

        let mut rf = |_offset: usize, data: &mut [u8]| {
            data[..20].copy_from_slice(&b"Hello Bare-Metal BLE"[..]);
            17
        };
        let mut wf = |offset: usize, data: &[u8]| {
            println!("RECEIVED: {} {:?}", offset, data);
        };

        let mut wf2 = |offset: usize, data: &[u8]| {
            println!("RECEIVED: {} {:?}", offset, data);
        };

        let mut rf3 = |_offset: usize, data: &mut [u8]| {
            data[..5].copy_from_slice(&b"Hola!"[..]);
            5
        };
        let mut wf3 = |offset: usize, data: &[u8]| {
            println!("RECEIVED: Offset {}, data {:?}", offset, data);
        };

        // usefull referece https://github.com/bjoernQ/esp32c3-ble-hid/blob/main/src/main.rs#L229-L284

        gatt!([service {
            uuid: "937312e0-2354-11eb-9f10-fbc30a62cf38",
            characteristics: [
                characteristic {
                    uuid: "937312e0-2354-11eb-9f10-fbc30a62cf38",
                    read: rf,
                    write: wf,
                },
                characteristic {
                    uuid: "957312e0-2354-11eb-9f10-fbc30a62cf38",
                    write: wf2,
                },
                characteristic {
                    name: "my_characteristic",
                    uuid: "987312e0-2354-11eb-9f10-fbc30a62cf38",
                    notify: true,
                    read: rf3,
                    write: wf3,
                },
            ],
        },]);

        let mut rng = bleps::no_rng::NoRng;
        let mut srv = AttributeServer::new(&mut ble, &mut gatt_attributes, &mut rng);

        let counter = RefCell::new(0u8);
        let counter = &counter;

        let mut notifier = || {
            // TODO how to check if notifications are enabled for the characteristic?
            // maybe pass something into the closure which just can query the characteristic value
            // probably passing in the attribute server won't work?

            async {
                pin_ref.borrow_mut().wait_for_rising_edge().await;
                let mut data = [0u8; 13];
                data.copy_from_slice(b"Notification0");
                {
                    let mut counter = counter.borrow_mut();
                    data[data.len() - 1] += *counter;
                    *counter = (*counter + 1) % 10;
                }
                NotificationData::new(my_characteristic_handle, &data)
            }
        };

        srv.run(&mut notifier).await.unwrap();
    }
}

Now I wanted to change the Server with the gatt macro to this:

        gatt!([
            service {
                uuid: "00001812-0000-1000-8000-00805f9b34fb",
                characteristics: [
                    characteristic {
                        name: "read_datastore",
                        uuid: "00002a4a-0000-1000-8000-00805f9b34fb",
                        read: sync_events_read,
                    },
                    characteristic {
                        name: "write_datastore",
                        uuid: "00002a4b-0000-1000-8000-00805f9b34fb",
                        write: sync_events_write,
                    },
                    characteristic {
                        name: "my_characteristic",
                        uuid: "00002a4c-0000-1000-8000-00805f9b34fb",
                        notify: true,
                        read: rf3,
                        write: wf3,
                    },
                ],
            },
            // Battery LVL, also see PDF for assigned number
            // https://forums.developer.apple.com/forums/thread/77866
            service {
                uuid: "180F", // Battery Service UUID (16-bit)
                characteristics: [characteristic {
                    uuid: "2A19", // Battery Level Characteristic UUID (16-bit)
                    read: read_battery_lvl
                }]
            },
        ]);

but it seems the Service including the characteristics never changes from the original first flashed profile. I tested this as I have a second esp32h2 and this one cannot change from these characteristics back to the example ones.

I do flash with the command "espflash flash --monitor --no-skip" and my .cargo/config.toml looks like this:

[target.riscv32imac-unknown-none-elf]
runner = "espflash flash --monitor --no-skip"
rustflags = [
  # "-C",
  # "link-arg=-Tlinkall.x",
  "-C",
  "link-arg=-Trom_functions.x",
  "-C",
  "force-frame-pointers",
]


[env]
ESP_LOGLEVEL = "INFO"
# ESP_LOGLEVEL = "DEBUG"

[build]
rustflags = [
  #required for WIFI
  # "-C",
  # "link-arg=-Tlinkall.x",
  "-C",
  "link-arg=-Trom_functions.x",
  # Required to obtain backtraces (e.g. when using the "esp-backtrace" crate.)
  # NOTE: May negatively impact performance of produced code
  "-C",
  "force-frame-pointers",
]

target = "riscv32imac-unknown-none-elf"


[unstable]
build-std = ["core"]

I also included my whole example project for reproducibility. Some guidance would be much appreciated.

esph2_example.zip

Fallible and async read/write callbacks

Would you be interested in generalizing AttData to allow errors and futures? The errors would involve changing AttData to be like

trait AttData {
  fn read(&mut self, offset: usize, data: &mut [u8]) -> Result<usize, AttErrorCode>;
  fn write(&mut self, offset: usize, data: &[u8]) -> Result<(), AttErrorCode>;
}

The async version might have to involve another trait, like bleps::async_attribute::Attribute or something.

You could still of course implement AttData for FnMut(usize, &[u8]) and similar, so it probably wouldn't cause much breakage.

notify characteristic not supported

    this.dataNotifyCharacteristic.startNotifications()
    this.dataNotifyCharacteristic.addEventListener('characteristicvaluechanged', (event) => {
      const value = event.target.value
      const payload = Buffer.from(value.buffer)
       ...

is GATT characteristic value changing supported?

ble.cmd_set_le_advertising_data(create_advertising_data(&[
            AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED),
            AdStructure::ServiceUuids16(&[
                Uuid::Uuid16(0x1809)
            ]),
            AdStructure::CompleteLocalName("BLE_TO_ISOTP"),
        ])).unwrap();

am I setting something wrong here?

List nearby BLE devices?

Hi! I notice examples here and in the esp-wifi crate for advertising device data. Is there a way to view nearby device advertisements? Thank you!

I'm experimenting along these lines:

    let bluetooth = peripherals.BT;

    let connector = BleConnector::new(&init, bluetooth);
    let hci = HciConnector::new(connector, esp_wifi::current_millis);
    let mut ble = Ble::new(&hci);

    ble.cmd_set_le_scan_rsp_data(
        // todo temp
        create_advertising_data(&[
            AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED),
            AdStructure::ServiceUuids16(&[Uuid::Uuid16(0x1809)]),
            // AdStructure::CompleteLocalName(examples_util::SOC_NAME),
            AdStructure::CompleteLocalName("TEST"),
        ]).unwrap()
    ).unwrap();


    println!("BLE init: {:?}", ble.init());

Notifications stopping after a write when the client is Linux

I have a very strange issue. On Linux, when I listen for notification on a characteristic, notifications works OK, but as soon as I write to any characteristics (even in a different service) they stop. Re-asking for notifications make them works again until the next write.

I've used both the btleplug library and the official bluer library.

If I use the ble_server example from esp32-nimble, it works as expected.

If I use the nRF Connect app on android as the client, it works as expected.

There is some strange interaction between the Linux Bluetooth stack and bleps.

Fix tests

Fix tests

Tests in bleps and bleps-macros broke ... should get fixed

Why does BLE use `&dyn`?

This is more a question than an issue, but is there a specific reason, why bleps doesn't use generics instead of a dynamic reference to an HCIConnection on the BLE struct?
If this has a specific reason, please let me know.
The BLE struct is likely only instantiated once in the entire application and monomorphization will likely even reduce code size here.

Uuid128 off by 1 error in advertising data?

 let connector = esp_wifi::ble::controller::BleConnector {};
        let hci = HciConnector::new(connector, esp_wifi::current_millis);
        let service_uuid = Uuid::Uuid128([0x00, 0x00, 0xab, 0xf0, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0x80, 0x5f, 0x9b, 0x34, 0xfb]);
        let mut ble = Ble::new(&hci);
        ble.init().unwrap();
        ble.cmd_set_le_advertising_parameters().unwrap();
        let advertising_data = create_advertising_data(&[
            AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED),
            AdStructure::ServiceUuids128(&[service_uuid]),
            AdStructure::CompleteLocalName("BLE_TO_ISOTP"),
        ]);
        ble.cmd_set_le_advertising_data(advertising_data).unwrap();
        ble.cmd_set_le_advertise_enable(true).unwrap();
Brandons-MacBook-Air:esp32-isotp-ble-bridge-rs brandonros 2022-10-26 11:14:41 $ cargo run --release
warning: Patch `embassy-embedded-hal v0.1.0 (https://github.com/embassy-rs/embassy#ce1cba76)` was not used in the crate graph.
Check that the patched package version and available features are compatible
with the dependency requirements. If the patch has a different version from
what is locked in the Cargo.lock file, run `cargo update` to use the new
version. This may also occur with an optional dependency that is not enabled.
   Compiling esp-wifi v0.1.0 (https://github.com/esp-rs/esp-wifi?branch=main#7adb1658)
   Compiling esp32_isotp_ble_bridge_rs v0.1.0 (/Users/brandonros/Desktop/esp32-isotp-ble-bridge-rs)
warning: unused variable: `wdt`
   --> src/main.rs:146:13
    |
146 |     let mut wdt = timer_group0.wdt;
    |             ^^^ help: if this is intentional, prefix it with an underscore: `_wdt`
    |
    = note: `#[warn(unused_variables)]` on by default

warning: variable does not need to be mutable
   --> src/main.rs:146:9
    |
146 |     let mut wdt = timer_group0.wdt;
    |         ----^^^
    |         |
    |         help: remove this `mut`
    |
    = note: `#[warn(unused_mut)]` on by default

warning: static `CHANNEL` is never used
  --> src/main.rs:34:8
   |
34 | static CHANNEL: StaticCell<Channel<NoopRawMutex, u32, 4>> = StaticCell::new();
   |        ^^^^^^^
   |
   = note: `#[warn(dead_code)]` on by default

warning: static `TIMER0` is never used
  --> src/main.rs:36:8
   |
36 | static TIMER0: Mutex<RefCell<Option<Timer<Timer0<TIMG0>>>>> = Mutex::new(RefCell::new(None));
   |        ^^^^^^

warning: static `TIMER1` is never used
  --> src/main.rs:37:8
   |
37 | static TIMER1: Mutex<RefCell<Option<Timer<Timer1<TIMG0>>>>> = Mutex::new(RefCell::new(None));
   |        ^^^^^^

warning: static `SERIAL0` is never used
  --> src/main.rs:38:8
   |
38 | static SERIAL0: Mutex<RefCell<Option<Serial<UART0>>>> = Mutex::new(RefCell::new(None));
   |        ^^^^^^^

warning: function `receiver_task` is never used
  --> src/main.rs:40:10
   |
40 | async fn receiver_task(receiver: Receiver<'static, NoopRawMutex, u32, 4>) {
   |          ^^^^^^^^^^^^^

warning: function `sender_task` is never used
  --> src/main.rs:59:10
   |
59 | async fn sender_task(sender: Sender<'static, NoopRawMutex, u32, 4>) {
   |          ^^^^^^^^^^^

warning: `esp32_isotp_ble_bridge_rs` (bin "esp32_isotp_ble_bridge_rs") generated 8 warnings
    Finished release [optimized] target(s) in 1.44s
     Running `espflash --monitor /dev/tty.usbserial-02728E37 --monitor-speed 115200 target/xtensa-esp32-none-elf/release/esp32_isotp_ble_bridge_rs`
Serial port: /dev/tty.usbserial-02728E37
Connecting...

Chip type:         ESP32 (revision 3)
Crystal frequency: 40MHz
Flash size:        16MB
Features:          WiFi, BT, Dual Core, 240MHz, Coding Scheme None
MAC address:       94:b9:7e:57:4b:18
App/part. size:    295984/16711680 bytes, 1.77%
[00:00:01] ########################################      16/16      segment 0x1000                                                                                                                                                                                                                                                                                                                                                                 
[00:00:00] ########################################       1/1       segment 0x8000                                                                                                                                                                                                                                                                                                                                                                 
[00:00:16] ########################################     156/156     segment 0x10000                                                                                                                                                                                                                                                                                                                                                                
Flashing has completed!
Commands:
    CTRL+R    Reset chip
    CTRL+C    Exit

ets Jul 29 2019 12:21:46
rst:0x1 (POWERON_RESET),boot:0x17 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0048,len:12
ho 0 tail 12 room 4
load:0x3fff0054,len:4800
load:0x40078000,len:17448
0x40078000 - r_rwip_reset
    at ??:??
load:0x4007c428,len:4840
0x4007c428 - r_rwip_reset
    at ??:??
entry 0x4007c6a0
0x4007c6a0 - r_rwip_reset
    at ??:??
WARN - coex_register_bt_cb 0x40081698
0x40081698 - coex_bt_callback
    at ??:??
WARN - coex_schm_register_btdm_callback 0x400df7dc
0x400df7dc - coex_schm_btdm_callback
    at ??:??
WARN - coex_wifi_channel_get
 
 
!! A panic occured in '/Users/brandonros/.rustup/toolchains/esp/lib/rustlib/src/rust/library/core/src/slice/index.rs', at line 73, column 5
 
PanicInfo {
    payload: Any { .. },
    message: Some(
        range end index 129 out of range for slice of length 128,
    ),
    location: Location {
        file: "/Users/brandonros/.rustup/toolchains/esp/lib/rustlib/src/rust/library/core/src/slice/index.rs",
        line: 73,
        col: 5,
    },
    can_unwind: true,
}
 
Backtrace:
 
0x400dc009
0x400dc009 - _ZN4core3ops8function6FnOnce9call_once17hf83c65e1c5683b9fE
    at ??:??
0x400dc039
0x400dc039 - _ZN4core10intrinsics17const_eval_select17h2e98052629cb9109E
    at ??:??
0x400dc095
0x400dc095 - _ZN4core5slice5index24slice_end_index_len_fail17h1a7d586e621fc8b7E
    at ??:??
0x400d7fca
0x400d7fca - _ZN5bleps12ad_structure23create_advertising_data17ha24b1c7a30ebd68cE
    at ??:??
0x400d14ae
0x400d14ae - _ZN97_$LT$core..future..from_generator..GenFuture$LT$T$GT$$u20$as$u20$core..future..future..Future$GT$4poll17h4c7b5d09e6303bf8E
    at ??:??
0x400d1c1c
0x400d1c1c - _ZN16embassy_executor3raw20TaskStorage$LT$F$GT$4poll17h57ceb5a97861a96dE.llvm.11765450987655502707
    at ??:??
0x400f7e27
0x400f7e27 - _ZN16embassy_executor3raw8Executor4poll17hb27cb61edfd0c27cE
    at ??:??
0x400d1c88
0x400d1c88 - _ZN16embassy_executor4arch8Executor3run17h50320a162ae4a970E
    at ??:??
0x400d1e45
0x400d1e45 - main
    at ??:??
0x400d947a
0x400d947a - Reset
    at ??:??

It's very hard to write a gatt service using `gatt!` macro

Hello, I'm trying to make a keyboard using esp32 + bleps. I found that it is quite hard to create a gatt service using bleps.

What I'm trying to create is like https://github.com/embassy-rs/nrf-softdevice/blob/master/examples/src/bin/ble_keyboard_peripheral_builder.rs#L289

The following is my code:

    let mut hid_info_fn = |_offset: usize, data: &mut [u8]| {
        data[0..4].copy_from_slice(&[0x1u8, 0x1u8, 0x00u8, 0x03u8]);
        4
    };
    let mut hid_desc_fn = |_offset: usize, data: &mut [u8]| {
        data[0..HID_REPORT_DESCRIPTOR.len()].copy_from_slice(HID_REPORT_DESCRIPTOR);
        HID_REPORT_DESCRIPTOR.len()
    };

    let mut hid_report_fn = |_offset: usize, data: &mut [u8]| {
        data[0..8].copy_from_slice(&[0, 0, 0x04, 0,0,0,0,0]);
        8
    };

    let keyboard_desc_value = [1,1u8];
    let mut kb_report = [0; 8];

    gatt!([service {
        uuid: "1812",
        characteristics: [
            characteristic {
                uuid: "2A4A",
                read: hid_info_fn,
            },
            characteristic {
                uuid: "2A4B",
                read: hid_desc_fn,
            },
            characteristic {
                uuid: "2A4D",
                notify: true,
                read: hid_report_fn,
                descriptors: [
                    descriptor {
                        uuid: "2908",
                        value: keyboard_desc_value,
                    },
                ],
            },
        ],
    },]);

I think I write the arguments right but the macro just doesn't compile. There is not any info from Rust compiler, even using RUSTFLAGS="-Zmacro-backtrace":

$ RUSTFLAGS="-Zmacro-backtrace" cargo build
error: Unexpected
   --> src\lib.rs:593:5
    |
593 | /     gatt!([service {
594 | |         uuid: "1812",
595 | |         characteristics: [
596 | |             characteristic {
...   |
615 | |         ],
616 | |     },]);
    | |        ^ in this macro invocation
    | |________|
    | 
    |
   ::: C:\Users\haobo\.cargo\git\checkouts\bleps-c5f04b27afb89012\9371d7d\bleps-macros\src\lib.rs:31:1
    |
31  |   pub fn gatt(input: TokenStream) -> TokenStream {
    |   ---------------------------------------------- in this expansion of `gatt!`

There is only an Unexpected error, I cannot get any info about what's unexpected and how to fix it. Any ideas? Thanks!

Bleps on nRF

Howdy!

I've been playing with bleps lately and got it running on nRF52 using their softdevice controller, but it required some modifications to bleps, which got me thinking about where to go from there. For instance, there are a few changes I would like to make:

  • Replace embedded-io/embedded-io-async with a HciDriver trait which operates on HCI packets. This maps better to non-serial HCI interfaces. (Serial adaption wrappers can be built)
  • Refactor the current blocking/async approach into something event-driven and build async on top of that, which should remove most of the duplication that's there now and perhaps work better with multiple connections.

I would like to hear your opinion on the above before making actual PRs. As specified on the front page, bleps is intended as a toy BLE stack, but are there any plans/goals beyond that? I'm interested in investing some effort to make it work well on nRF.

Support async

esp-wifi recently migrated to embassy-net, which is awesome! Would there be any interest in getting bleps to use async?

How to achieve parallel connections?

First of all, thank you so much for your great work!

I'm running bleps with embassy on an ESP32-C3 and was wondering if it's possible to open multiple BLE connections in parallel. Do you have any experience of this?

The chip should support it, but I'm too unfamiliar with Bluetooth to know at what level the parallelism should take place. Would it be enough to give multiple Ble instances access to the same embedded_io_async::{Read, Write} BleConnector instances?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.