nRF24-ruby 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/bin/nRF24_udp.rb ADDED
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env ruby
2
+ #encoding: UTF-8
3
+
4
+
5
+ require "pp"
6
+ require 'socket'
7
+ require 'json'
8
+ require 'uri'
9
+ require 'ipaddr'
10
+ require 'time'
11
+ require 'thread'
12
+
13
+
14
+ if File.file? './lib/nRF24-ruby.rb'
15
+ require './lib/nRF24-ruby.rb'
16
+ puts "using local lib"
17
+ else
18
+ require 'nRF24-ruby'
19
+ end
20
+
21
+ puts "\nPure Ruby nRF24 <-> UDP Bridge Starting..."
22
+
23
+ http=true
24
+ #http=false
25
+
26
+ if http
27
+ puts "Loading Http-server.. hold on.."
28
+ require 'minimal-http-ruby'
29
+ minimal_http_server http_port: 8088, http_path: "./http/"
30
+ puts "\n"
31
+ end
32
+
33
+ def open_port uri_s
34
+ begin
35
+ uri = URI.parse(uri_s)
36
+ if uri.scheme== 'udp'
37
+ return [UDPSocket.new,uri.host,uri.port]
38
+ else
39
+ raise "Error: Cannot open socket for '#{uri_s}', unsupported scheme: '#{uri.scheme}'"
40
+ end
41
+ rescue => e
42
+ pp e.backtrace
43
+ raise "Error: Cannot open socket for '#{uri_s}': #{e}"
44
+ end
45
+ end
46
+
47
+ def poll_packet socket
48
+ begin
49
+ r,stuff=socket.recvfrom_nonblock(200) #get_packet --high level func!
50
+ client_ip=stuff[2]
51
+ client_port=stuff[1]
52
+ return [r,client_ip,client_port]
53
+ rescue IO::WaitReadable
54
+ sleep 0.1
55
+ rescue => e
56
+ puts "Error: receive thread died: #{e}"
57
+ pp e.backtrace
58
+ end
59
+ return nil
60
+ end
61
+
62
+ def poll_packet_block socket
63
+ #decide how to get data -- UDP-socket or FM-radio
64
+ r,stuff=socket.recvfrom(200) #get_packet --high level func!
65
+ client_ip=stuff[2]
66
+ client_port=stuff[1]
67
+ return [r,client_ip,client_port]
68
+ end
69
+
70
+ def send_raw_packet msg,socket,server,port
71
+ if socket
72
+ if socket.class.name=="UDPSocket"
73
+ socket.send(msg, 0, server, port)
74
+ else
75
+ a=msg.unpack('c*')
76
+ socket.send_q << {msg: a, to: server, socket: port,ack:false}
77
+ end
78
+ #MqttSN::hexdump msg
79
+ else
80
+ puts "Error: no socket at send_raw_packet"
81
+ end
82
+ end
83
+
84
+
85
+ r0=NRF24.new id: :eka, ce: 22,cs: 27, irq: 17, chan:4, ack: false, mac: "A7:A7",mac_header: true
86
+ r1=NRF24.new id: :toka, ce: 24,cs: 23, irq: 22, chan: 4, ack: false, mac: "A5:A5",mac_header: true
87
+
88
+ s0,s0_host,s0_port=open_port("udp://20.20.20.21:1882") # our port for forwarder/broker connection
89
+ s1,s1_host,s1_port=open_port("udp://20.20.20.21:1882")
90
+ s1.bind("0.0.0.0",5555) # our port for clients
91
+ pp s0
92
+ pp s1
93
+ puts "Main Loop Starts:"
94
+
95
+ loopc=0;
96
+ sc=0;
97
+
98
+ def poll_packet_radio r
99
+ if not r.recv_q.empty? #get packets from broker's radio and send them to udp broker/forwader
100
+ msg=r.recv_q.pop
101
+ pac=msg[:msg].pack("c*")
102
+ len=msg[:msg][0]
103
+ if len<1 or len>30
104
+ puts "crap #{msg}"
105
+ return nil
106
+ end
107
+ if msg[:checksum]!=msg[:check]
108
+ puts "checksum error #{msg}"
109
+ return nil
110
+ end
111
+ pac=pac[0...len]
112
+ #puts "got #{msg}, '#{pac}'"
113
+ return [pac,msg[:from],msg[:socket] ]
114
+ end
115
+ return nil
116
+ end
117
+
118
+ # client s1 <-> r0
119
+
120
+ # forwarder s0 <-> r1
121
+
122
+ loop do
123
+ begin
124
+ if pac=poll_packet(s1) #get packets from client's via udp (s1) and send them to forwarder's radio
125
+ r,@client_ip,@client_port=pac
126
+ puts "UDP1(#{@client_ip}:#{@client_port})->RAD0(#{r1.mac}:#{4}): #{pac}"
127
+ #r0.send_q << {msg: msg, to: r1.mac, socket: 3,ack:false}
128
+ send_raw_packet r,r0,r1.mac,4
129
+ end
130
+
131
+ if pac=poll_packet_radio(r1) #this is forwarder receiving the packet from client -- and sendig it to broker at s0
132
+ r,client_ip,client_port=pac
133
+ puts "RAD1(#{client_ip}:#{client_port})->UDP0(#{s0_host}:#{s0_port}): #{pac}"
134
+ send_raw_packet r,s0,s0_host,s0_port
135
+ end
136
+
137
+
138
+ if pac=poll_packet(s0) #get packets from broker ... send to client via radio
139
+ r,client_ip,client_port=pac
140
+ puts "UDP0(#{client_ip}:#{client_port})->RAD1(#{r0.mac}:#{3}): #{pac}"
141
+ send_raw_packet r,r1,r0.mac,3
142
+ end
143
+
144
+ if pac=poll_packet_radio(r0) #this is client listening to radio r0 and getting the packet (via s1)
145
+ r,client_ip,client_port=pac
146
+ puts "RAD0(#{client_ip}:#{client_port})->UDP1(#{@client_ip}:#{@client_port}): #{pac}"
147
+ send_raw_packet r,s1,@client_ip,@client_port
148
+ end
149
+
150
+ rescue => e
151
+ puts "Error: receive thread died: #{e}"
152
+ pp e.backtrace
153
+ end
154
+ sleep 0.01
155
+ end
156
+
@@ -11,6 +11,7 @@ end
11
11
  puts "\nPure Ruby nRF24L01 Driver Starting..."
12
12
 
13
13
  http=true
14
+ #http=false
14
15
 
15
16
  if http
16
17
  puts "Loading Http-server.. hold on.."
@@ -19,39 +20,40 @@ if http
19
20
  puts "\n"
20
21
  end
21
22
 
22
- bmac="B2:B2:B3"
23
+ bmac="C7:C7:C7"
23
24
  NRF24::set_bmac bmac
24
- r0=NRF24.new id: :eka, ce: 22,cs: 27, irq: 17, chan:3, ack: false, mac: "00:A7:A7"
25
- r1=NRF24.new id: :toka, ce: 24,cs: 23, irq: 22, chan: 3, ack: false, mac: "00:A5:A5"
25
+ r0=NRF24.new id: :eka, ce: 22,cs: 27, irq: 17, chan:4, ack: false, mac: "A7:A7",mac_header: true
26
+ r1=NRF24.new id: :toka, ce: 24,cs: 23, irq: 22, chan: 4, ack: false, mac: "A5:A5",mac_header: true
26
27
 
27
28
  puts "Main Loop Starts: bmac: #{NRF24::get_bmac}"
28
29
 
29
30
  loopc=0;
30
31
  sc=0;
31
-
32
+ lista=[]
32
33
  loop do
33
34
  if (loopc%4)==0
34
35
  msg=[]
35
- str=sprintf "testing %5.5d -- testing?",sc%10000
36
+ str=sprintf "%5.5d",sc
37
+ lista<<sc
36
38
  sc+=1
37
39
  str.each_byte do |b|
38
40
  msg<<b
39
41
  end
40
42
  if sc&2==0
41
43
  if sc&1==0
42
- r0.send_q << {msg: msg, tx_mac: bmac}
44
+ r0.send_q << {msg: msg, socket: :broadcast}
43
45
  else
44
- r1.send_q << {msg: msg, tx_mac: bmac}
46
+ r1.send_q << {msg: msg, socket: :broadcast}
45
47
  end
46
48
  else
47
49
  if sc&1==0
48
50
  #r0.send_q << {msg: msg, tx_mac: r1.mac,ack:true}
49
- r0.send_q << {msg: msg, tx_mac: "FF:A5:A5",ack:true}
51
+ r0.send_q << {msg: msg, to: r1.mac, socket: 3,ack:true}
50
52
  else
51
- r1.send_q << {msg: msg, tx_mac: r0.mac,ack:true}
53
+ r1.send_q << {msg: msg, to: r0.mac, socket: 4,ack:true}
52
54
  end
53
55
  end
54
- NRF24::note "sent '#{str}' to #{sc&1}"
56
+ NRF24::note "lista:#{lista.size}"
55
57
  end
56
58
  loopc+=1
57
59
  while not r1.recv_q.empty?
@@ -59,25 +61,37 @@ loop do
59
61
  #puts "got 1 #{got}"
60
62
  msg=""
61
63
  len=0
62
- got.each_with_index do |b,i|
63
- msg[i]=b.chr
64
+ got[:msg].each_with_index do |b,i|
65
+ break if b==0x00
66
+ msg[i]=b.chr
64
67
  end
65
- NRF24::note "got #{msg} from 1!"
68
+ got[:msg]=msg
69
+ s=got[:msg].to_i
70
+ lista-=[s]
71
+ NRF24::note "i #{got}"
66
72
  end
67
73
  while not r0.recv_q.empty?
68
74
  got=r0.recv_q.pop
69
75
  #puts "got 0 #{got}"
70
76
  msg=""
71
77
  len=0
72
- got.each_with_index do |b,i|
73
- msg[i]=b.chr
78
+ got[:msg].each_with_index do |b,i|
79
+ msg[i]=b.chr if b!=0x00
74
80
  end
75
- NRF24::note "got #{msg} from 0!"
81
+ got[:msg]=msg
82
+ s=got[:msg].to_i
83
+ lista-=[s]
84
+ NRF24::note "i #{got}"
76
85
  end
77
86
  if not http
78
- pp r0.json
79
- pp r1.json
87
+ if NRF24::get_log.size>0
88
+ e=NRF24::get_log[0]
89
+ if e[:text][/lista/]
90
+ puts e
91
+ end
92
+ NRF24::get_log.shift
93
+ end
80
94
  end
81
- sleep 0.1
95
+ sleep 0.05
82
96
  end
83
97
 
data/http/json/action.rb CHANGED
@@ -20,7 +20,7 @@ def json_action request,args,session,event
20
20
  d.get_regs true
21
21
  else
22
22
  puts "initing #{d}"
23
- d.hw_init chan: chan, ack: aa, rf_dr: args['rf_dr'], rf_pwr: args['rf_pwr'], lna_hcurr: args['lna_hcurr']
23
+ d.hw_init chan: chan, ack: aa, rf_dr: args['rf_dr'], rf_pwr: args['rf_pwr'], lna_hcurr: args['lna_hcurr'],mac_header: true
24
24
  d.get_regs true
25
25
  end
26
26
  end
data/lib/nRF24-ruby.rb CHANGED
@@ -3,14 +3,16 @@
3
3
 
4
4
  require 'pp'
5
5
  require 'thread'
6
+ require 'time'
6
7
  require 'pi_piper'
8
+ require 'zlib'
7
9
  include PiPiper
8
10
 
9
11
  class NRF24
10
12
  @@all=[]
11
13
  @@PAYLOAD_SIZE=32
12
14
  @@SPI_CLOCK=250000
13
-
15
+
14
16
  @@regs={
15
17
  CONFIG: {address: 0x00,len:7},
16
18
  EN_AA: {address: 0x01,len:6},
@@ -36,7 +38,7 @@ class NRF24
36
38
  RX_PW_P4: {address: 0x15, format: :dec,hide: true},
37
39
  RX_PW_P5: {address: 0x16, format: :dec,hide: true},
38
40
  FIFO_STATUS: {address: 0x17, poll: 1, len:7},
39
- DYNPD: {address: 0x1C,len:6},
41
+ DYNPD: {address: 0x1C,len:6},
40
42
  FEATURE: {address: 0x1D,len:3},
41
43
  }
42
44
 
@@ -53,13 +55,12 @@ class NRF24
53
55
  ACTIVATE2: 0x73,
54
56
  }
55
57
 
56
- @@sem=Mutex.new
58
+ @@sem=Mutex.new
57
59
  @@log=[]
58
60
  @@bmac="45:45:45:45:45"
59
61
 
60
62
  def self.set_bmac mac
61
63
  @@bmac=mac
62
- puts "set bmac to #{mac}"
63
64
  end
64
65
 
65
66
  def self.note str,*args
@@ -72,7 +73,7 @@ class NRF24
72
73
  puts "note dies: #{e} '#{str}'"
73
74
  end
74
75
  end
75
-
76
+
76
77
  def get_ccode c
77
78
  ccode=@@cmds[c]
78
79
  if not ccode
@@ -95,8 +96,8 @@ class NRF24
95
96
  status=0
96
97
  cc=get_ccode(c)
97
98
  @@sem.synchronize do
98
- @cs.off
99
- PiPiper::Spi.begin do
99
+ @cs.off
100
+ PiPiper::Spi.begin do
100
101
  clock(@@SPI_CLOCK)
101
102
  status=write cc
102
103
  data.each do |byte|
@@ -116,14 +117,14 @@ class NRF24
116
117
  cc=get_ccode(:R_REGISTER) +i
117
118
  @@sem.synchronize do
118
119
  @cs.off
119
- PiPiper::Spi.begin do
120
+ PiPiper::Spi.begin do
120
121
  clock(@@SPI_CLOCK)
121
122
  status=write cc
122
123
  if bytes==1
123
124
  data=write(0xff)
124
125
  else
125
126
  data=[]
126
- bytes.times do
127
+ bytes.times do
127
128
  data << write(0xff)
128
129
  end
129
130
  end
@@ -157,6 +158,16 @@ class NRF24
157
158
  [@s[:status]]
158
159
  end
159
160
 
161
+ def calc_crc str
162
+ checksum=0
163
+ str.unpack("c*").each do |c|
164
+ checksum+=c.ord
165
+ end
166
+ checksum&=0xff
167
+ #puts "cc calc: '#{str}'' -> #{checksum}"
168
+ return checksum
169
+ end
170
+
160
171
  def send packet,hash={}
161
172
  pac=Array.new(@@PAYLOAD_SIZE, 0)
162
173
  packet.each_with_index do |byte,i|
@@ -164,12 +175,16 @@ class NRF24
164
175
  end
165
176
  @ce.off
166
177
  wreg :CONFIG,0x0a
178
+ #prepend our mac?
179
+ if @s[:params][:mac_header]
180
+ checksum= calc_crc(pac.pack("c*"))
181
+ pac= NRF24::mac2a(@mac, short:true)+[checksum]+pac
182
+ pac=pac[0...32]
183
+ end
167
184
  if hash[:ack] and @s[:params][:ack]
168
185
  cmd :W_TX_PAYLOAD,pac
169
- #puts "with ack"
170
186
  else
171
187
  cmd :W_TX_PAYLOAD_NOACK,pac
172
- #puts "with NOack"
173
188
  end
174
189
  @ce.on
175
190
  sleep 0.001
@@ -178,13 +193,6 @@ class NRF24
178
193
  @ce.on
179
194
  end
180
195
 
181
- def recv
182
- fifo_status,_=rreg :FIFO_STATUS
183
- if (fifo_status & 0x01) == 0x01
184
- puts "on dataa"
185
- end
186
- end
187
-
188
196
  def get_regs all
189
197
  @@regs.each do |k,r|
190
198
  next if not r[:poll] and not all
@@ -198,34 +206,48 @@ class NRF24
198
206
  begin
199
207
  loop do
200
208
  donesome=false
201
-
209
+ cflag=0
202
210
  s,d,b=rreg :FIFO_STATUS
203
211
  if (s&0x40) == 0x40
204
- NRF24::note "got RX_DR --received something"
205
- wreg :STATUS,0x40
212
+ #NRF24::note "got RX_DR --received something"
213
+ cflag|=0x40
206
214
  end
207
215
  if (s&0x20) == 0x20
208
- NRF24::note "got TX_DS --sent something"
216
+ #NRF24::note "got TX_DS --sent something"
209
217
  wreg :STATUS,0x20
218
+ cflag|=0x20
210
219
  end
211
220
  if (s&0x10) == 0x10
212
221
  NRF24::note "****************** got MAX_RT --send fails..."
213
- wreg :STATUS,0x10
222
+ cflag|=0x10
214
223
  @s[:sfail]+=1
215
224
  end
216
- if (d&0x01)==0x00
225
+ wreg(:STATUS,cflag) if cflag>0
226
+ while (d&0x01)==0x00
227
+ @s[:rfull]+=1 if (d&0x02)==0x02
217
228
  pipe=(s>>1)&0x05
218
- NRF24::note "pipe: #{pipe}"
219
- ret=cmd :R_RX_PAYLOAD,Array.new(@@PAYLOAD_SIZE, 0xff)
220
- @recv_q<<ret
221
- @s[:rcnt]+=1
222
- donesome=true
223
- end
224
- if (d&0x02)==0x02
229
+ if pipe==0
230
+ socket=:broadcast
231
+ else
232
+ socket=pipe-1
233
+ end
225
234
  ret=cmd :R_RX_PAYLOAD,Array.new(@@PAYLOAD_SIZE, 0xff)
226
- @recv_q<<ret
235
+ if @s[:params][:mac_header]
236
+ sender=NRF24::a2mac(ret[0..1])
237
+ checksum=ret[2]
238
+ ret.shift(3)
239
+ check= calc_crc(ret.pack("c*"))
240
+ if check!=checksum
241
+ puts "Error: Checksum error! Message Ignored!"
242
+ s,d,b=rreg :FIFO_STATUS
243
+ next
244
+ end
245
+ end
246
+ msg={msg:ret,socket:socket,from:sender,to:@mac,dir: :in,checksum:checksum,check:check}
247
+ @recv_q << msg
248
+ NRF24::note "i #{msg}"
227
249
  @s[:rcnt]+=1
228
- @s[:rfull]+=1
250
+ s,d,b=rreg :FIFO_STATUS
229
251
  donesome=true
230
252
  end
231
253
 
@@ -233,7 +255,6 @@ class NRF24
233
255
  if not @send_q.empty?
234
256
  if (d&0x20)==0x00
235
257
 
236
-
237
258
  s,d,b=rreg :OBSERVE_TX
238
259
  if (d&0x0f)!=0x00
239
260
  NRF24::note "got ARC_CNT:#{d&0x0f}******************"
@@ -241,9 +262,18 @@ class NRF24
241
262
  end
242
263
 
243
264
  msg=@send_q.pop
244
- wreg :TX_ADDR,NRF24::mac2a(msg[:tx_mac])
245
- #puts "send mac: #{msg[:tx_mac]}"
265
+ #puts "msg:#{msg}"
266
+ if msg[:socket]==:broadcast
267
+ to=NRF24::mac2a(NRF24::get_bmac)
268
+ else
269
+ to=NRF24::mac2a(msg[:to],socket: msg[:socket])
270
+ end
271
+ wreg :TX_ADDR, to
272
+ msg[:from]=@mac
273
+ msg[:dir]=:out
246
274
  send msg[:msg], ack:msg[:ack]
275
+ #msg[:msg]=msg[:msg].pack("c*")
276
+ NRF24::note "o #{msg}"
247
277
  @s[:scnt]+=1
248
278
  end
249
279
  end
@@ -299,19 +329,30 @@ class NRF24
299
329
  @@regs
300
330
  end
301
331
 
302
- def json
332
+ def json
303
333
  @s
304
334
  end
305
335
 
306
- def self.get_log
336
+ def self.get_log
307
337
  @@log
308
338
  end
309
339
 
310
- def self.mac2a mac
340
+ def self.a2mac a,hash={}
341
+ mac=""
342
+ a.each do |e|
343
+ mac+=":" if mac!=""
344
+ mac+=sprintf "%02X",e
345
+ end
346
+ mac
347
+ end
348
+
349
+ def self.mac2a mac,hash={}
311
350
  a=[]
312
351
  mac.split(":").each do |b|
313
352
  a<<b.hex
314
353
  end
354
+ a.unshift hash[:socket]||0 if a.size==2 and not hash[:short]
355
+ #pp a
315
356
  a
316
357
  end
317
358
 
@@ -326,7 +367,7 @@ class NRF24
326
367
  scnt: 0,
327
368
  sarc: 0,
328
369
  sfail: 0,
329
- }
370
+ }
330
371
  @id=hash[:id]
331
372
  wreg :CONFIG,0x0b
332
373
  rf_dr=(hash[:rf_dr]||1).to_i&0x01
@@ -354,37 +395,27 @@ class NRF24
354
395
  wreg :RX_PW_P5,@@PAYLOAD_SIZE
355
396
  wreg :TX_ADDR,NRF24::mac2a(@@bmac)
356
397
  wreg :RX_ADDR_P0,NRF24::mac2a(@@bmac)
357
- wreg :RX_ADDR_P2,0xfc
358
- wreg :RX_ADDR_P3,0xfd
359
- wreg :RX_ADDR_P4,0xfe
360
- wreg :RX_ADDR_P5,0xff
398
+
361
399
  if hash[:mac] #keep old if not defined
362
- wreg :RX_ADDR_P1,NRF24::mac2a(hash[:mac])
400
+ wreg :RX_ADDR_P1,NRF24::mac2a(hash[:mac])
363
401
  @mac=hash[:mac]
364
402
  else
365
- s,d,bytes,code =rreg :RX_ADDR_P1
366
- mac=""
367
- d.each do |b|
368
- mac+=":" if mac!=""
369
- mac+=sprintf "%02X",b
370
- end
371
- @mac=mac
372
- end
373
403
 
374
- # if hash[:ack]
375
- # cmd :ACTIVATE,[ 0]
376
- # else
377
- cmd :ACTIVATE,[ get_ccode(:ACTIVATE2)]
378
- # end
404
+ end
405
+ wreg :RX_ADDR_P2,1
406
+ wreg :RX_ADDR_P3,2
407
+ wreg :RX_ADDR_P4,3
408
+ wreg :RX_ADDR_P5,4
409
+ cmd :ACTIVATE,[ get_ccode(:ACTIVATE2)]
379
410
 
380
411
  cmd :FLUSH_TX
381
412
  cmd :FLUSH_RX
382
413
  end
383
414
 
384
415
  def initialize(hash={})
385
- @semh=Mutex.new
416
+ @semh=Mutex.new
417
+
386
418
 
387
-
388
419
  @ce=PiPiper::Pin.new(:pin => hash[:ce], :direction => :out)
389
420
  @cs=PiPiper::Pin.new(:pin => hash[:cs], :direction => :out)
390
421
 
@@ -397,7 +428,8 @@ class NRF24
397
428
 
398
429
  hw_init hash
399
430
 
400
- @server_t=rf_server
431
+ @server_t=rf_server
432
+ @server_t.priority=100
401
433
  @monitor_t=do_monitor
402
434
  end
403
435
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nRF24-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-11-28 00:00:00.000000000 Z
12
+ date: 2014-11-30 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: minimal-http-ruby
@@ -46,12 +46,14 @@ dependencies:
46
46
  description: ! 'Pure Ruby Driver and Utilitity with Http-server for the Ultra Cheap
47
47
  Radio Chip nRF24 '
48
48
  email: jalopuuverstas@gmail.com
49
- executables: []
49
+ executables:
50
+ - nRF24_udp.rb
50
51
  extensions: []
51
52
  extra_rdoc_files: []
52
53
  files:
53
54
  - lib/nRF24-ruby.rb
54
55
  - examples/nRF24-demo.rb
56
+ - bin/nRF24_udp.rb
55
57
  - http/json/logger.rb
56
58
  - http/json/nRF24.rb
57
59
  - http/json/action.rb